diff --git a/.gitignore b/.gitignore index 3c057d9bda4..5f0817bd269 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,4 @@ target/ .DS_Store -.allure/ src/main/proto/ diff --git a/docs/application-settings.md b/docs/application-settings.md index 4999cd293a5..4abd9ac5bc2 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -54,6 +54,9 @@ Keep in mind following restrictions: to `sfN.enforce` value. - `privacy.gdpr.purpose-one-treatment-interpretation` - option that allows to skip the Purpose one enforcement workflow. Values: ignore, no-access-allowed, access-allowed. +- `privacy.gdpr.purposes.p4.eid.require_consent` - if equals to `true`, transmitting EIDs require P4 legal basis unless excepted. +- `privacy.gdpr.purposes.p4.eid.exceptions` - list of EID sources that are excepted from P4 enforcement and will be transmitted if any P2-P10 is consented. +- `privacy.gdpr.purposes.p4.eid.activity_transition` - defaults to `true`. If `true` and transmitEids is not specified, but transmitUfpd is specified, then the logic of transmitUfpd is used. This is to avoid breaking changes to existing configurations. The default value of the flag will be changed in a future release. - `metrics.verbosity-level` - defines verbosity level of metrics for this account, overrides `metrics.accounts` application settings configuration. - `analytics.auction-events.` - defines which channels are supported by analytics for this account - `analytics.modules..*` - space for `module-name` analytics module specific configuration, may be of any shape diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index d190470096f..30748f5061f 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 6e070d5e652..0bb36be1d10 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 45cbae3b016..2fe803c1ba6 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index cff942ae523..436fd2a33d3 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 623c74b038a..6a0d60bc391 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT ../../extra/pom.xml @@ -43,7 +43,7 @@ org.prebid prebid-server - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT org.projectlombok diff --git a/extra/pom.xml b/extra/pom.xml index ae0241b02a0..0ef8a94ae56 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server-aggregator - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT pom diff --git a/pom.xml b/pom.xml index cef81d13bbe..abbd1a513e1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 2.12.0-SNAPSHOT + 3.0.0-SNAPSHOT extra/pom.xml @@ -57,7 +57,7 @@ 4.13.2 - 5.9.2 + 5.10.2 4.11.0 3.24.2 2.35.1 @@ -69,7 +69,6 @@ 3.0.14 1.17.4 5.14.0 - 2.19.0 1.9.9.1 1.12.14 @@ -84,7 +83,6 @@ ${maven-surefire-plugin.version} 0.40.2 1.13.1 - 2.10.0 false true false @@ -189,6 +187,11 @@ netty-transport-native-epoll linux-x86_64 + + io.netty + netty-transport-native-epoll + linux-aarch_64 + org.apache.commons commons-lang3 @@ -587,12 +590,6 @@ ${mockserver.version} test - - io.qameta.allure - allure-java-commons - ${allure.version} - test - @@ -912,7 +909,6 @@ -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" - target/allure-results ${mockserver.version} ${project.version} 2 @@ -956,11 +952,6 @@ - - io.qameta.allure - allure-maven - ${allure-maven.version} - org.apache.maven.plugins maven-compiler-plugin @@ -985,17 +976,6 @@ - - org.jacoco - jacoco-maven-plugin - - - - report - - - - diff --git a/src/main/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtils.java b/src/main/java/org/prebid/server/activity/ActivitiesConfigResolver.java similarity index 66% rename from src/main/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtils.java rename to src/main/java/org/prebid/server/activity/ActivitiesConfigResolver.java index eccc75dee0f..df267438a4a 100644 --- a/src/main/java/org/prebid/server/activity/utils/AccountActivitiesConfigurationUtils.java +++ b/src/main/java/org/prebid/server/activity/ActivitiesConfigResolver.java @@ -1,6 +1,7 @@ -package org.prebid.server.activity.utils; +package org.prebid.server.activity; -import org.prebid.server.activity.Activity; +import io.vertx.core.logging.LoggerFactory; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.settings.model.activity.AccountActivityConfiguration; @@ -14,19 +15,43 @@ import java.util.Optional; import java.util.stream.Collectors; -public class AccountActivitiesConfigurationUtils { +public class ActivitiesConfigResolver { - private AccountActivitiesConfigurationUtils() { + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(ActivitiesConfigResolver.class)); + + private final double logSamplingRate; + + public ActivitiesConfigResolver(double logSamplingRate) { + this.logSamplingRate = logSamplingRate; + } + + public Account resolve(Account account) { + if (!isInvalidActivitiesConfiguration(account)) { + return account; + } + + conditionalLogger.warn( + "Activity configuration for account %s contains conditional rule with empty array." + .formatted(account.getId()), + logSamplingRate); + + final AccountPrivacyConfig accountPrivacyConfig = account.getPrivacy(); + return account.toBuilder() + .privacy(accountPrivacyConfig.toBuilder() + .activities(removeInvalidRules(accountPrivacyConfig.getActivities())) + .build()) + .build(); } - public static boolean isInvalidActivitiesConfiguration(Account account) { + private static boolean isInvalidActivitiesConfiguration(Account account) { return Optional.ofNullable(account) .map(Account::getPrivacy) .map(AccountPrivacyConfig::getActivities) .stream() .map(Map::values) .flatMap(Collection::stream) - .anyMatch(AccountActivitiesConfigurationUtils::containsInvalidRule); + .anyMatch(ActivitiesConfigResolver::containsInvalidRule); } private static boolean containsInvalidRule(AccountActivityConfiguration accountActivityConfiguration) { @@ -34,7 +59,7 @@ private static boolean containsInvalidRule(AccountActivityConfiguration accountA .map(AccountActivityConfiguration::getRules) .stream() .flatMap(Collection::stream) - .anyMatch(AccountActivitiesConfigurationUtils::isInvalidConditionRule); + .anyMatch(ActivitiesConfigResolver::isInvalidConditionRule); } private static boolean isInvalidConditionRule(AccountActivityRuleConfig rule) { @@ -63,13 +88,13 @@ private static boolean isEmptyNotNull(Collection collection) { return collection != null && collection.isEmpty(); } - public static Map removeInvalidRules( + private static Map removeInvalidRules( Map activitiesConfiguration) { return activitiesConfiguration.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, - entry -> AccountActivitiesConfigurationUtils.removeInvalidRules(entry.getValue()))); + entry -> removeInvalidRules(entry.getValue()))); } private static AccountActivityConfiguration removeInvalidRules(AccountActivityConfiguration activityConfiguration) { diff --git a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java new file mode 100644 index 00000000000..ddafc42fd53 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java @@ -0,0 +1,99 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import io.vertx.core.Future; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.IpAddress; +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; +import org.prebid.server.execution.Timeout; +import org.prebid.server.geolocation.GeoLocationService; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.metric.Metrics; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountSettings; + +import java.util.Objects; +import java.util.Optional; + +public class GeoLocationServiceWrapper { + + private static final Logger logger = LoggerFactory.getLogger(GeoLocationServiceWrapper.class); + + private final GeoLocationService geoLocationService; + private final Ortb2ImplicitParametersResolver implicitParametersResolver; + private final Metrics metrics; + + public GeoLocationServiceWrapper(GeoLocationService geoLocationService, + Ortb2ImplicitParametersResolver implicitParametersResolver, + Metrics metrics) { + + this.geoLocationService = geoLocationService; + this.implicitParametersResolver = Objects.requireNonNull(implicitParametersResolver); + this.metrics = Objects.requireNonNull(metrics); + } + + //todo: account settings will work as expected if the default account resolving refactoring is done + public Future lookup(AuctionContext auctionContext) { + final Account account = auctionContext.getAccount(); + final Device device = auctionContext.getBidRequest().getDevice(); + final HttpRequestContext requestContext = auctionContext.getHttpRequest(); + final Timeout timeout = auctionContext.getTimeoutContext().getTimeout(); + + final boolean isGeoLookupEnabled = Optional.ofNullable(account.getSettings()) + .map(AccountSettings::getGeoLookup) + .map(BooleanUtils::isTrue) + .orElse(false); + + return isGeoLookupEnabled + ? doLookup(getIpAddress(device, requestContext), getCountry(device), timeout).otherwiseEmpty() + : Future.succeededFuture(); + } + + public Future doLookup(String ipAddress, String requestCountry, Timeout timeout) { + if (geoLocationService == null || ipAddress == null || StringUtils.isNotBlank(requestCountry)) { + return Future.failedFuture("Geolocation lookup is skipped"); + } + return geoLocationService.lookup(ipAddress, timeout) + .onSuccess(geoInfo -> metrics.updateGeoLocationMetric(true)) + .onFailure(this::logError); + } + + private String getCountry(Device device) { + return Optional.ofNullable(device) + .map(Device::getGeo) + .map(Geo::getCountry) + .filter(StringUtils::isNotBlank) + .orElse(null); + } + + private String getIpAddress(Device device, HttpRequestContext request) { + final Optional optionalDevice = Optional.ofNullable(device); + return optionalDevice.map(Device::getIp) + .filter(StringUtils::isNotBlank) + .or(() -> optionalDevice + .map(Device::getIpv6) + .filter(StringUtils::isNotBlank)) + .or(() -> ipFromHeader(request)) + .orElse(null); + } + + private Optional ipFromHeader(HttpRequestContext request) { + final IpAddress headerIp = implicitParametersResolver.findIpFromRequest(request); + return Optional.ofNullable(headerIp) + .map(IpAddress::getIp); + } + + private void logError(Throwable error) { + final String message = "Geolocation lookup failed: " + error.getMessage(); + logger.warn(message); + logger.debug(message, error); + + metrics.updateGeoLocationMetric(false); + } +} diff --git a/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java b/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java index fb1cc09c74b..f923711447b 100644 --- a/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java +++ b/src/main/java/org/prebid/server/auction/gpp/SetuidGppService.java @@ -1,6 +1,7 @@ package org.prebid.server.auction.gpp; import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.gpp.model.GppContextCreator; import org.prebid.server.auction.gpp.model.GppContextWrapper; @@ -44,7 +45,7 @@ private static GppContextWrapper contextFrom(PrivacyContext privacyContext) { private static Integer toInt(String string) { try { - return string != null ? Integer.parseInt(string) : null; + return StringUtils.isNotBlank(string) ? Integer.parseInt(string) : null; } catch (NumberFormatException e) { return null; } diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 34820274916..2012897f44f 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -58,6 +58,7 @@ public class AuctionContext { ActivityInfrastructure activityInfrastructure; + @JsonIgnore GeoInfo geoInfo; HookExecutionContext hookExecutionContext; @@ -115,6 +116,12 @@ public AuctionContext with(DebugContext debugContext) { .build(); } + public AuctionContext with(GeoInfo geoInfo) { + return this.toBuilder() + .geoInfo(geoInfo) + .build(); + } + public AuctionContext withRequestRejected() { return this.toBuilder() .requestRejected(true) diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java index 1f1db91f7aa..84f9695c3ea 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AmpPrivacyContextFactory.java @@ -61,7 +61,8 @@ public Future contextFrom(AuctionContext auctionContext) { accountGdprConfig(account), requestType, requestLogInfo(requestType, bidRequest, account.getId()), - auctionContext.getTimeoutContext().getTimeout()) + auctionContext.getTimeoutContext().getTimeout(), + auctionContext.getGeoInfo()) .map(tcfContext -> logWarnings(auctionContext.getDebugWarnings(), tcfContext)) .map(tcfContext -> PrivacyContext.of(strippedPrivacy, tcfContext, tcfContext.getIpAddress())); } diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java index debcefe86e4..087b72a67c6 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/AuctionPrivacyContextFactory.java @@ -57,7 +57,8 @@ public Future contextFrom(AuctionContext auctionContext) { accountGdprConfig(account), requestType, requestLogInfo(requestType, bidRequest, account.getId()), - auctionContext.getTimeoutContext().getTimeout()) + auctionContext.getTimeoutContext().getTimeout(), + auctionContext.getGeoInfo()) .map(tcfContext -> logWarnings(auctionContext.getDebugWarnings(), tcfContext)) .map(tcfContext -> PrivacyContext.of(privacy, tcfContext, tcfContext.getIpAddress())); } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 3ea03835656..76c8b921196 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.DebugResolver; import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.OrtbTypesResolver; import org.prebid.server.auction.PriceGranularity; @@ -96,6 +97,7 @@ public class AmpRequestFactory { private final AmpPrivacyContextFactory ampPrivacyContextFactory; private final DebugResolver debugResolver; private final JacksonMapper mapper; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, @@ -107,7 +109,8 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, FpdResolver fpdResolver, AmpPrivacyContextFactory ampPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); @@ -120,6 +123,7 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, this.debugResolver = Objects.requireNonNull(debugResolver); this.ampPrivacyContextFactory = Objects.requireNonNull(ampPrivacyContextFactory); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); } /** @@ -142,6 +146,12 @@ public Future fromRequest(RoutingContext routingContext, long st .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + .compose(auctionContext -> gppService.contextFrom(auctionContext) .map(auctionContext::with)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 309f1f26576..1e0369b75b1 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -7,6 +7,7 @@ import io.vertx.core.Future; import io.vertx.ext.web.RoutingContext; import org.prebid.server.auction.DebugResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.OrtbTypesResolver; @@ -48,6 +49,7 @@ public class AuctionRequestFactory { private final DebugResolver debugResolver; private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); @@ -63,7 +65,8 @@ public AuctionRequestFactory(long maxRequestSize, OrtbTypesResolver ortbTypesResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); @@ -78,6 +81,7 @@ public AuctionRequestFactory(long maxRequestSize, this.auctionPrivacyContextFactory = Objects.requireNonNull(auctionPrivacyContextFactory); this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); } /** @@ -105,6 +109,12 @@ public Future fromRequest(RoutingContext routingContext, long st .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + .compose(auctionContext -> gppService.contextFrom(auctionContext) .map(auctionContext::with)) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java index 9ffd8304caf..c1b346b0f73 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -284,7 +284,7 @@ private String sanitizeIp(String ip, IpAddress.IP version) { return ipAddress != null && ipAddress.getVersion() == version ? ipAddress.getIp() : null; } - private IpAddress findIpFromRequest(HttpRequestContext request) { + public IpAddress findIpFromRequest(HttpRequestContext request) { final CaseInsensitiveMultiMap headers = request.getHeaders(); final String remoteHost = request.getRemoteHost(); final List requestIps = paramsExtractor.ipFrom(headers, remoteHost); diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 8a062380041..61fe2069a60 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -87,7 +87,6 @@ public class Ortb2RequestFactory { private static final ConditionalLogger EMPTY_ACCOUNT_LOGGER = new ConditionalLogger("empty_account", logger); private static final ConditionalLogger UNKNOWN_ACCOUNT_LOGGER = new ConditionalLogger("unknown_account", logger); - private final boolean enforceValidAccount; private final int timeoutAdjustmentFactor; private final double logSamplingRate; private final List blacklistedAccounts; @@ -104,8 +103,7 @@ public class Ortb2RequestFactory { private final CountryCodeMapper countryCodeMapper; private final Metrics metrics; - public Ortb2RequestFactory(boolean enforceValidAccount, - int timeoutAdjustmentFactor, + public Ortb2RequestFactory(int timeoutAdjustmentFactor, double logSamplingRate, List blacklistedAccounts, UidsCookieService uidsCookieService, @@ -125,7 +123,6 @@ public Ortb2RequestFactory(boolean enforceValidAccount, throw new IllegalArgumentException("Expected timeout adjustment factor should be in [0, 100]."); } - this.enforceValidAccount = enforceValidAccount; this.timeoutAdjustmentFactor = timeoutAdjustmentFactor; this.logSamplingRate = logSamplingRate; this.blacklistedAccounts = Objects.requireNonNull(blacklistedAccounts); @@ -209,30 +206,60 @@ public Future validateRequest(BidRequest bidRequest, : Future.succeededFuture(bidRequest); } + public Future enrichBidRequestWithGeolocationData(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Device device = bidRequest.getDevice(); + final GeoInfo geoInfo = auctionContext.getGeoInfo(); + final Geo geo = ObjectUtil.getIfNotNull(device, Device::getGeo); + + final UpdateResult resolvedCountry = resolveCountry(geo, geoInfo); + final UpdateResult resolvedRegion = resolveRegion(geo, geoInfo); + + if (!resolvedCountry.isUpdated() && !resolvedRegion.isUpdated()) { + return Future.succeededFuture(bidRequest); + } + + final Geo updatedGeo = Optional.ofNullable(geo) + .map(Geo::toBuilder) + .orElseGet(Geo::builder) + .country(resolvedCountry.getValue()) + .region(resolvedRegion.getValue()) + .build(); + + final Device updatedDevice = Optional.ofNullable(device) + .map(Device::toBuilder) + .orElseGet(Device::builder) + .geo(updatedGeo) + .build(); + + return Future.succeededFuture(bidRequest.toBuilder().device(updatedDevice).build()); + + } + public Future enrichBidRequestWithAccountAndPrivacyData(AuctionContext auctionContext) { final BidRequest bidRequest = auctionContext.getBidRequest(); final Account account = auctionContext.getAccount(); final PrivacyContext privacyContext = auctionContext.getPrivacyContext(); + final ExtRequest requestExt = bidRequest.getExt(); + final ExtRequest enrichedRequestExt = enrichExtRequest(requestExt, account); + final Device device = bidRequest.getDevice(); final Device enrichedDevice = enrichDevice(device, privacyContext); final Regs regs = bidRequest.getRegs(); final Regs enrichedRegs = enrichRegs(regs, privacyContext, account); - final ExtRequest requestExt = bidRequest.getExt(); - final ExtRequest enrichedRequestExt = enrichExtRequest(requestExt, account); - if (enrichedRequestExt == null && enrichedDevice == null && enrichedRegs == null) { return Future.succeededFuture(bidRequest); } - final BidRequest enrichedBidRequest = bidRequest.toBuilder() + return Future.succeededFuture(bidRequest.toBuilder() + .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) .device(ObjectUtils.defaultIfNull(enrichedDevice, device)) .regs(ObjectUtils.defaultIfNull(enrichedRegs, regs)) - .ext(ObjectUtils.defaultIfNull(enrichedRequestExt, requestExt)) - .build(); - return Future.succeededFuture(enrichedBidRequest); + .build()); + } private static Regs enrichRegs(Regs regs, PrivacyContext privacyContext, Account account) { @@ -410,20 +437,26 @@ private String validateIfAccountBlacklisted(String accountId) { return accountId; } - private Future loadAccount(Timeout timeout, - HttpRequestContext httpRequest, - String accountId) { - - final Future accountFuture = StringUtils.isBlank(accountId) - ? responseForEmptyAccount(httpRequest) - : applicationSettings.getAccountById(accountId, timeout) - .compose(this::ensureAccountActive, - exception -> accountFallback(exception, accountId, httpRequest)); + private Future loadAccount(Timeout timeout, HttpRequestContext httpRequest, String accountId) { + if (StringUtils.isBlank(accountId)) { + EMPTY_ACCOUNT_LOGGER.warn(accountErrorMessage("Account not specified", httpRequest), logSamplingRate); + } - return accountFuture + return applicationSettings.getAccountById(accountId, timeout) + .compose(this::ensureAccountActive) + .recover(exception -> wrapFailure(exception, accountId, httpRequest)) .onFailure(ignored -> metrics.updateAccountRequestRejectedByInvalidAccountMetrics(accountId)); } + private Future ensureAccountActive(Account account) { + final String accountId = account.getId(); + + return account.getStatus() == AccountStatus.inactive + ? Future.failedFuture( + new UnauthorizedAccountException("Account %s is inactive".formatted(accountId), accountId)) + : Future.succeededFuture(account); + } + /** * Extracts publisher id either from {@link BidRequest}.app.publisher or {@link BidRequest}.site.publisher. * If neither is present returns empty string. @@ -458,23 +491,10 @@ private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { return extPublisherPrebid != null ? StringUtils.stripToNull(extPublisherPrebid.getParentAccount()) : null; } - private Future responseForEmptyAccount(HttpRequestContext httpRequest) { - EMPTY_ACCOUNT_LOGGER.warn(accountErrorMessage("Account not specified", httpRequest), logSamplingRate); - return responseForUnknownAccount(StringUtils.EMPTY); - } - - private static String accountErrorMessage(String message, HttpRequestContext httpRequest) { - return "%s, Url: %s and Referer: %s".formatted( - message, - httpRequest.getAbsoluteUri(), - httpRequest.getHeaders().get(HttpUtil.REFERER_HEADER)); - } - - private Future accountFallback(Throwable exception, - String accountId, - HttpRequestContext httpRequest) { - - if (exception instanceof PreBidException) { + private Future wrapFailure(Throwable exception, String accountId, HttpRequestContext httpRequest) { + if (exception instanceof UnauthorizedAccountException) { + return Future.failedFuture(exception); + } else if (exception instanceof PreBidException) { UNKNOWN_ACCOUNT_LOGGER.warn(accountErrorMessage(exception.getMessage(), httpRequest), 100); } else { metrics.updateAccountRequestRejectedByFailedFetch(accountId); @@ -482,24 +502,15 @@ private Future accountFallback(Throwable exception, logger.debug("Error occurred while fetching account", exception); } - // hide all errors occurred while fetching account - return responseForUnknownAccount(accountId); + return Future.failedFuture( + new UnauthorizedAccountException("Unauthorized account id: " + accountId, accountId)); } - private Future responseForUnknownAccount(String accountId) { - return enforceValidAccount - ? Future.failedFuture(new UnauthorizedAccountException( - "Unauthorized account id: " + accountId, accountId)) - : Future.succeededFuture(Account.empty(accountId)); - } - - private Future ensureAccountActive(Account account) { - final String accountId = account.getId(); - - return account.getStatus() == AccountStatus.inactive - ? Future.failedFuture(new UnauthorizedAccountException( - "Account %s is inactive".formatted(accountId), accountId)) - : Future.succeededFuture(account); + private static String accountErrorMessage(String message, HttpRequestContext httpRequest) { + return "%s, Url: %s and Referer: %s".formatted( + message, + httpRequest.getAbsoluteUri(), + httpRequest.getHeaders().get(HttpUtil.REFERER_HEADER)); } private ExtRequest enrichExtRequest(ExtRequest ext, Account account) { @@ -597,9 +608,10 @@ private Device enrichDevice(Device device, PrivacyContext privacyContext) { final boolean shouldUpdateIpV6 = ipV6 != null && !Objects.equals(ipV6InRequest, ipV6); final Geo geo = ObjectUtil.getIfNotNull(device, Device::getGeo); + final GeoInfo geoInfo = privacyContext.getTcfContext().getGeoInfo(); - final UpdateResult resolvedCountry = resolveCountry(geo, privacyContext); - final UpdateResult resolvedRegion = resolveRegion(geo, privacyContext); + final UpdateResult resolvedCountry = resolveCountry(geo, geoInfo); + final UpdateResult resolvedRegion = resolveRegion(geo, geoInfo); if (shouldUpdateIpV4 || shouldUpdateIpV6 || resolvedCountry.isUpdated() || resolvedRegion.isUpdated()) { final Device.DeviceBuilder deviceBuilder = device != null ? device.toBuilder() : Device.builder(); @@ -629,10 +641,9 @@ private Device enrichDevice(Device device, PrivacyContext privacyContext) { return null; } - private UpdateResult resolveCountry(Geo geo, PrivacyContext privacyContext) { - final String countryInRequest = geo != null ? geo.getCountry() : null; + private UpdateResult resolveCountry(Geo originalGeo, GeoInfo geoInfo) { + final String countryInRequest = originalGeo != null ? originalGeo.getCountry() : null; - final GeoInfo geoInfo = privacyContext.getTcfContext().getGeoInfo(); final String alpha2CountryCode = geoInfo != null ? geoInfo.getCountry() : null; final String alpha3CountryCode = countryCodeMapper.mapToAlpha3(alpha2CountryCode); @@ -641,11 +652,10 @@ private UpdateResult resolveCountry(Geo geo, PrivacyContext privacyConte : UpdateResult.unaltered(countryInRequest); } - private static UpdateResult resolveRegion(Geo geo, PrivacyContext privacyContext) { - final String regionInRequest = geo != null ? geo.getRegion() : null; + private static UpdateResult resolveRegion(Geo originalGeo, GeoInfo geoInfo) { + final String regionInRequest = originalGeo != null ? originalGeo.getRegion() : null; final String upperCasedRegionInRequest = StringUtils.upperCase(regionInRequest); - final GeoInfo geoInfo = privacyContext.getTcfContext().getGeoInfo(); final String region = geoInfo != null ? geoInfo.getRegion() : null; final String upperCasedRegion = StringUtils.upperCase(region); diff --git a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java index 1000a39c929..31e150f78cf 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java @@ -16,6 +16,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.DebugResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.CachedDebugLog; @@ -60,6 +61,7 @@ public class VideoRequestFactory { private final AuctionPrivacyContextFactory auctionPrivacyContextFactory; private final DebugResolver debugResolver; private final JacksonMapper mapper; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; public VideoRequestFactory(int maxRequestSize, boolean enforceStoredRequest, @@ -70,7 +72,8 @@ public VideoRequestFactory(int maxRequestSize, Ortb2ImplicitParametersResolver paramsResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { this.enforceStoredRequest = enforceStoredRequest; this.maxRequestSize = maxRequestSize; @@ -81,6 +84,7 @@ public VideoRequestFactory(int maxRequestSize, this.auctionPrivacyContextFactory = Objects.requireNonNull(auctionPrivacyContextFactory); this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); this.escapeLogCacheRegexPattern = StringUtils.isNotBlank(escapeLogCacheRegex) ? Pattern.compile(escapeLogCacheRegex) @@ -123,6 +127,12 @@ public Future> fromRequest(RoutingContext routingC .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> geoLocationServiceWrapper.lookup(auctionContext) + .map(auctionContext::with)) + + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithGeolocationData(auctionContext) + .map(auctionContext::with)) + .compose(auctionContext -> ortb2RequestFactory.activityInfrastructureFrom(auctionContext) .map(auctionContext::with)) diff --git a/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java b/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java index e0198ac0d20..3a968fc8192 100644 --- a/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java +++ b/src/main/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25Converter.java @@ -155,8 +155,7 @@ private static Video modifyVideo(Video video) { video.getPodseq(), video.getRqddurs(), video.getSlotinpod(), - video.getMincpmpersec(), - video.getPlcmt()) + video.getMincpmpersec()) ? video.toBuilder() .maxseq(null) @@ -166,7 +165,6 @@ private static Video modifyVideo(Video video) { .rqddurs(null) .slotinpod(null) .mincpmpersec(null) - .plcmt(null) .build() : null; diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java index 3f6a30f38cf..e97db214f3c 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -2,18 +2,22 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -24,10 +28,9 @@ import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; -import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusResponse; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusNetBid; -import org.prebid.server.bidder.adnuntius.model.util.AdsUnitWithImpId; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -39,6 +42,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -56,8 +60,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.IntStream; -import java.util.stream.Stream; +import java.util.function.Function; +import java.util.stream.Collectors; public class AdnuntiusBidder implements Bidder { @@ -68,7 +72,6 @@ public class AdnuntiusBidder implements Bidder { private static final String TARGET_ID_DELIMITER = "-"; private static final String DEFAULT_PAGE = "unknown"; private static final String DEFAULT_NETWORK = "default"; - private static final String URL_NO_COOKIES_PARAMETER = "noCookies"; private static final BigDecimal PRICE_MULTIPLIER = BigDecimal.valueOf(1000); private final String endpointUrl; @@ -95,53 +98,16 @@ public Result>> makeHttpRequests(BidRequest r return Result.withError(BidderError.badInput(e.getMessage())); } - noCookies = resolveIsNoCookies(extImpAdnuntius); + noCookies = noCookies || resolveIsNoCookies(extImpAdnuntius); final String network = resolveNetwork(extImpAdnuntius); - networkToAdUnits.computeIfAbsent(network, n -> new ArrayList<>()) - .add(makeAdnuntiusAdUnit(imp, extImpAdnuntius)); + networkToAdUnits.computeIfAbsent(network, ignored -> new ArrayList<>()) + .add(makeAdUnit(imp, extImpAdnuntius)); } return Result.withValues(createHttpRequests(networkToAdUnits, request, noCookies)); } - private static AdnuntiusAdUnit makeAdnuntiusAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { - final String auId = extImpAdnuntius.getAuId(); - return AdnuntiusAdUnit.builder() - .auId(auId) - .targetId(auId + TARGET_ID_DELIMITER + imp.getId()) - .dimensions(createDimensions(imp)) - .maxDeals(resolveMaxDeals(extImpAdnuntius)) - .build(); - } - - private static List> createDimensions(Imp imp) { - final Banner banner = imp.getBanner(); - - if (CollectionUtils.isNotEmpty(banner.getFormat())) { - final List> formats = new ArrayList<>(); - for (Format format : banner.getFormat()) { - if (format.getW() != null && format.getH() != null) { - formats.add(List.of(format.getW(), format.getH())); - } - } - return formats; - } - - if (banner.getW() != null && banner.getH() != null) { - return Collections.singletonList(List.of(banner.getW(), banner.getH())); - } - - return null; - } - - private static Integer resolveMaxDeals(ExtImpAdnuntius extImpAdnuntius) { - if (extImpAdnuntius.getMaxDeals() != null && extImpAdnuntius.getMaxDeals() > 0) { - return extImpAdnuntius.getMaxDeals(); - } - return null; - } - private static void validateImp(Imp imp) { if (imp.getBanner() == null) { throw new PreBidException("Fail on Imp.Id=%s: Adnuntius supports only Banner".formatted(imp.getId())); @@ -159,42 +125,84 @@ private ExtImpAdnuntius parseImpExt(Imp imp) { private static boolean resolveIsNoCookies(ExtImpAdnuntius extImpAdnuntius) { return Optional.of(extImpAdnuntius) .map(ExtImpAdnuntius::getNoCookies) - .filter(BooleanUtils::isTrue) - .isPresent(); + .map(BooleanUtils::isTrue) + .orElse(false); } private static String resolveNetwork(ExtImpAdnuntius extImpAdnuntius) { return Optional.of(extImpAdnuntius) .map(ExtImpAdnuntius::getNetwork) - .filter(StringUtils::isNoneEmpty) + .filter(StringUtils::isNotEmpty) .orElse(DEFAULT_NETWORK); } - private List> createHttpRequests(Map> networkToAdUnits, - BidRequest request, Boolean noCookies) { + private static AdnuntiusAdUnit makeAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { + final String auId = extImpAdnuntius.getAuId(); + return AdnuntiusAdUnit.builder() + .auId(auId) + .targetId(targetId(auId, imp.getId())) + .dimensions(createDimensions(imp.getBanner())) + .maxDeals(resolveMaxDeals(extImpAdnuntius)) + .build(); + } - final List> adnuntiusRequests = new ArrayList<>(); + private static String targetId(String auId, String impId) { + return auId + TARGET_ID_DELIMITER + impId; + } - final AdnuntiusMetaData metaData = createMetaData(request.getUser()); - final String page = extractPage(request.getSite()); - final String uri = createUri(request, noCookies); - final Device device = request.getDevice(); + private static List> createDimensions(Banner banner) { + final List> formats = new ArrayList<>(); - for (List adUnits : networkToAdUnits.values()) { - final AdnuntiusRequest adnuntiusRequest = AdnuntiusRequest.of(adUnits, metaData, page); - adnuntiusRequests.add(createHttpRequest(adnuntiusRequest, uri, device)); + final List bannerFormat = ListUtils.emptyIfNull(banner.getFormat()); + for (Format format : bannerFormat) { + final Integer w = format.getW(); + final Integer h = format.getH(); + if (w != null && h != null) { + formats.add(List.of(w, h)); + } + } + if (!formats.isEmpty()) { + return formats; } - return adnuntiusRequests; + final Integer w = banner.getW(); + final Integer h = banner.getH(); + if (w != null && h != null) { + formats.add(List.of(w, h)); + } + + return formats.isEmpty() ? null : formats; } - private static AdnuntiusMetaData createMetaData(User user) { - final String userId = ObjectUtil.getIfNotNull(user, User::getId); - return StringUtils.isNotBlank(userId) ? AdnuntiusMetaData.of(userId) : null; + private static Integer resolveMaxDeals(ExtImpAdnuntius extImpAdnuntius) { + final Integer maxDeals = extImpAdnuntius.getMaxDeals(); + return maxDeals != null && maxDeals > 0 ? maxDeals : null; } - private static String extractPage(Site site) { - return StringUtils.defaultIfBlank(ObjectUtil.getIfNotNull(site, Site::getPage), DEFAULT_PAGE); + private List> createHttpRequests(Map> networkToAdUnits, + BidRequest request, + boolean noCookies) { + + final Site site = request.getSite(); + + final String uri = createUri(request, noCookies); + final String page = extractPage(site); + final ObjectNode data = extractData(site); + final AdnuntiusMetaData metaData = createMetaData(request.getUser()); + + final List> adnuntiusRequests = new ArrayList<>(); + + for (List adUnits : networkToAdUnits.values()) { + final AdnuntiusRequest adnuntiusRequest = AdnuntiusRequest.builder() + .adUnits(adUnits) + .context(page) + .keyValue(data) + .metaData(metaData) + .build(); + adnuntiusRequests.add(createHttpRequest(adnuntiusRequest, uri, request.getDevice())); + } + + return adnuntiusRequests; } private String createUri(BidRequest bidRequest, Boolean noCookies) { @@ -204,14 +212,17 @@ private String createUri(BidRequest bidRequest, Boolean noCookies) { .addParameter("tzo", getTimeZoneOffset()); final String gdpr = extractGdpr(bidRequest.getRegs()); - final String consent = extractConsent(bidRequest.getUser()); - if (StringUtils.isNoneEmpty(gdpr, consent)) { + if (StringUtils.isNotEmpty(gdpr)) { uriBuilder.addParameter("gdpr", gdpr); + } + + final String consent = extractConsent(bidRequest.getUser()); + if (StringUtils.isNotEmpty(consent)) { uriBuilder.addParameter("consentString", consent); } if (noCookies || extractNoCookies(bidRequest.getDevice())) { - uriBuilder.addParameter(URL_NO_COOKIES_PARAMETER, "true"); + uriBuilder.addParameter("noCookies", "true"); } return uriBuilder.build().toString(); @@ -225,36 +236,76 @@ private String getTimeZoneOffset() { } private static String extractGdpr(Regs regs) { - final Integer gdpr = ObjectUtil.getIfNotNull(ObjectUtil.getIfNotNull(regs, Regs::getExt), ExtRegs::getGdpr); - return gdpr != null ? gdpr.toString() : null; + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .map(Objects::toString) + .orElse(null); } private static String extractConsent(User user) { - return ObjectUtil.getIfNotNull(ObjectUtil.getIfNotNull(user, User::getExt), ExtUser::getConsent); + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(null); } - private static Boolean extractNoCookies(Device device) { + private static boolean extractNoCookies(Device device) { return Optional.ofNullable(device) .map(Device::getExt) .map(FlexibleExtension::getProperties) - .map(properties -> properties.get(URL_NO_COOKIES_PARAMETER)) + .map(properties -> properties.get("noCookies")) .filter(JsonNode::isBoolean) - .map(JsonNode::asBoolean) + .map(JsonNode::booleanValue) .orElse(false); } - private HttpRequest createHttpRequest(AdnuntiusRequest adnuntiusRequest, String uri, + private static String extractPage(Site site) { + return Optional.ofNullable(site) + .map(Site::getPage) + .filter(StringUtils::isNotEmpty) + .orElse(DEFAULT_PAGE); + } + + private static ObjectNode extractData(Site site) { + return Optional.ofNullable(site) + .map(Site::getExt) + .map(ExtSite::getData) + .orElse(null); + } + + private static AdnuntiusMetaData createMetaData(User user) { + final Optional userOptional = Optional.ofNullable(user); + return userOptional + .map(User::getId) + .filter(StringUtils::isNotEmpty) + .or(() -> userOptional + .map(User::getExt) + .map(ExtUser::getEids) + .filter(CollectionUtils::isNotEmpty) + .map(eids -> eids.get(0)) + .map(Eid::getUids) + .filter(CollectionUtils::isNotEmpty) + .map(uids -> uids.get(0)) + .map(Uid::getId)) + .map(AdnuntiusMetaData::of) + .orElse(null); + } + + private HttpRequest createHttpRequest(AdnuntiusRequest adnuntiusRequest, + String uri, Device device) { + return HttpRequest.builder() .method(HttpMethod.POST) - .headers(getHeaders(device)) + .headers(headers(device)) .uri(uri) .body(mapper.encodeToBytes(adnuntiusRequest)) .payload(adnuntiusRequest) .build(); } - private MultiMap getHeaders(Device device) { + private MultiMap headers(Device device) { final MultiMap headers = HttpUtil.headers(); if (device != null) { @@ -281,76 +332,62 @@ private List extractBids(BidRequest bidRequest, AdnuntiusResponse adn return Collections.emptyList(); } - final List adsUnits = adnuntiusResponse.getAdsUnits(); - final List imps = bidRequest.getImp(); - if (adsUnits.size() > imps.size()) { - throw new PreBidException("Impressions count is less then ads units count."); - } + final Map targetIdToAdsUnit = adnuntiusResponse.getAdsUnits().stream() + .filter(AdnuntiusBidder::validateAdsUnit) + .collect(Collectors.toMap( + AdnuntiusAdsUnit::getTargetId, + Function.identity(), + (first, second) -> second)); - final List validAdsUnitToImp = IntStream.range(0, adsUnits.size()) - .mapToObj(i -> AdsUnitWithImpId.of(adsUnits.get(i), imps.get(i), parseImpExt(imps.get(i)))) - .filter(adsUnitWithImpId -> validateAdsUnit(adsUnitWithImpId.getAdsUnit())) - .toList(); + String currency = null; + final List bids = new ArrayList<>(); - if (validAdsUnitToImp.isEmpty()) { - return Collections.emptyList(); - } + for (Imp imp : bidRequest.getImp()) { + final ExtImpAdnuntius extImpAdnuntius = parseImpExt(imp); + final String targetId = targetId(extImpAdnuntius.getAuId(), imp.getId()); - final String currency = extractCurrency(validAdsUnitToImp); - final Stream generalBids = validAdsUnitToImp.stream() - .map(adsUnitWithImpId -> makeGeneralBid(adsUnitWithImpId, currency)); + final AdnuntiusAdsUnit adsUnit = targetIdToAdsUnit.get(targetId); + if (adsUnit == null) { + continue; + } - final Stream dealBids = validAdsUnitToImp.stream() - .filter(adsUnitWithImpId -> CollectionUtils.isNotEmpty(adsUnitWithImpId.getAdsUnit().getDeals())) - .map(adsUnitWithImpId -> makeDealsBid(adsUnitWithImpId, currency)) - .filter(Objects::nonNull); + final AdnuntiusAd ad = adsUnit.getAds().get(0); + final String impId = imp.getId(); + final String bidType = extImpAdnuntius.getBidType(); + currency = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getCurrency); - return Stream.concat(generalBids, dealBids).toList(); - } + bids.add(createBid(ad, adsUnit.getHtml(), impId, bidType)); - private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { - final List ads = ObjectUtil.getIfNotNull(adsUnit, AdnuntiusAdsUnit::getAds); - return CollectionUtils.isNotEmpty(ads) && ads.get(0) != null; - } - - private static String extractCurrency(List adsUnits) { - final AdnuntiusBid bid = adsUnits.get(adsUnits.size() - 1).getAdsUnit().getAds().get(0).getBid(); - return ObjectUtil.getIfNotNull(bid, AdnuntiusBid::getCurrency); - } + for (AdnuntiusAd deal : ListUtils.emptyIfNull(adsUnit.getDeals())) { + bids.add(createBid(deal, deal.getHtml(), impId, bidType)); + } + } - private BidderBid makeGeneralBid(AdsUnitWithImpId adsUnitWithImpId, String currency) { - final AdnuntiusAdsUnit adsUnit = adsUnitWithImpId.getAdsUnit(); - final AdnuntiusAd ad = adsUnit.getAds().get(0); - final Bid bid = createBid(adsUnit, adsUnitWithImpId.getImp(), adsUnitWithImpId.getExtImpAdnuntius(), ad); - return BidderBid.of(bid, BidType.banner, currency); + final String lastCurrency = currency; + return bids.stream() + .map(bid -> BidderBid.of(bid, BidType.banner, lastCurrency)) + .toList(); } - private BidderBid makeDealsBid(AdsUnitWithImpId adsUnitWithImpId, String currency) { - final AdnuntiusAdsUnit adsUnit = adsUnitWithImpId.getAdsUnit(); - return adsUnit.getDeals().stream() - .map(adnuntiusAd -> - createBid(adsUnit, - adsUnitWithImpId.getImp(), - adsUnitWithImpId.getExtImpAdnuntius(), - adnuntiusAd)) - .map(bid -> BidderBid.of(bid, BidType.banner, currency)) - .findAny() - .orElse(null); + private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { + final List ads = adsUnit != null ? adsUnit.getAds() : null; + return CollectionUtils.isNotEmpty(ads) && ads.get(0) != null; } - private static Bid createBid(AdnuntiusAdsUnit adsUnit, Imp imp, ExtImpAdnuntius extImpAdnuntius, AdnuntiusAd ad) { + private static Bid createBid(AdnuntiusAd ad, String adm, String impId, String bidType) { final String adId = ad.getAdId(); + return Bid.builder() .id(adId) - .impid(imp.getId()) + .impid(impId) .w(parseMeasure(ad.getCreativeWidth())) .h(parseMeasure(ad.getCreativeHeight())) .adid(adId) + .dealid(ad.getDealId()) .cid(ad.getLineItemId()) .crid(ad.getCreativeId()) - .price(resolvePrice(ad, extImpAdnuntius.getBidType())) - .dealid(ad.getDealId()) - .adm(adsUnit.getHtml()) + .price(resolvePrice(ad, bidType)) + .adm(adm) .adomain(extractDomain(ad.getDestinationUrls())) .build(); } @@ -370,10 +407,10 @@ private static BigDecimal resolvePrice(AdnuntiusAd ad, String bidType) { amount = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getAmount); } if (StringUtils.endsWithIgnoreCase(bidType, "net")) { - amount = ObjectUtil.getIfNotNull(ad.getAdnuntiusNetBid(), AdnuntiusNetBid::getAmount); + amount = ObjectUtil.getIfNotNull(ad.getNetBid(), AdnuntiusNetBid::getAmount); } if (StringUtils.endsWithIgnoreCase(bidType, "gross")) { - amount = ObjectUtil.getIfNotNull(ad.getAdnuntiusGrossBid(), AdnuntiusGrossBid::getAmount); + amount = ObjectUtil.getIfNotNull(ad.getGrossBid(), AdnuntiusGrossBid::getAmount); } return amount != null ? amount.multiply(PRICE_MULTIPLIER) : BigDecimal.ZERO; @@ -384,7 +421,7 @@ private static List extractDomain(Map destinationUrls) { .filter(Objects::nonNull) .map(url -> url.split("/")) .filter(splintedUrl -> splintedUrl.length >= 2) - .map(splintedUrl -> splintedUrl[2].replaceAll("www\\.", "")) + .map(splintedUrl -> StringUtils.replace(splintedUrl[2], "www.", "")) .toList(); } } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java index d02557c690d..b077a3dce5f 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusMetaData.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.adnuntius.model.request; -import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Value; @Value(staticConstructor = "of") public class AdnuntiusMetaData { - @JsonInclude(JsonInclude.Include.NON_EMPTY) String usi; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java index ed3fd313542..eafd24facbf 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java @@ -2,11 +2,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; import java.util.List; -@Value(staticConstructor = "of") +@Builder(toBuilder = true) +@Value public class AdnuntiusRequest { @JsonProperty("adUnits") @@ -18,4 +21,7 @@ public class AdnuntiusRequest { @JsonInclude(JsonInclude.Include.NON_EMPTY) String context; + + @JsonProperty("kv") + ObjectNode keyValue; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java index 59a61a06579..47ff2c30f04 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java @@ -13,10 +13,10 @@ public class AdnuntiusAd { AdnuntiusBid bid; @JsonProperty("netBid") - AdnuntiusNetBid adnuntiusNetBid; + AdnuntiusNetBid netBid; @JsonProperty("grossBid") - AdnuntiusGrossBid adnuntiusGrossBid; + AdnuntiusGrossBid grossBid; @JsonProperty("dealId") String dealId; @@ -36,6 +36,8 @@ public class AdnuntiusAd { @JsonProperty("lineItemId") String lineItemId; + String html; + @JsonProperty("destinationUrls") Map destinationUrls; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/util/AdsUnitWithImpId.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/util/AdsUnitWithImpId.java deleted file mode 100644 index 339df114d43..00000000000 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/util/AdsUnitWithImpId.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.bidder.adnuntius.model.util; - -import com.iab.openrtb.request.Imp; -import lombok.Value; -import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; -import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; - -@Value(staticConstructor = "of") -public class AdsUnitWithImpId { - - AdnuntiusAdsUnit adsUnit; - - Imp imp; - - ExtImpAdnuntius extImpAdnuntius; -} diff --git a/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java b/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java index 2cb736f2aa3..f78d6c34f49 100644 --- a/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java +++ b/src/main/java/org/prebid/server/bidder/adprime/AdprimeBidder.java @@ -122,36 +122,34 @@ public final Result> makeBids(BidderCall httpCall, B try { final List errors = new ArrayList<>(); final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + return Result.of(extractBids(bidResponse, errors), errors); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse, - List errors) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse, errors); + return bidsFromResponse(bidResponse, errors); } - private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse, - List errors) { + private static List bidsFromResponse(BidResponse bidResponse, List errors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> resolveBidderBid(bid, bidResponse.getCur(), bidRequest.getImp(), errors)) + .map(bid -> resolveBidderBid(bid, bidResponse.getCur(), errors)) .filter(Objects::nonNull) .toList(); } - private static BidderBid resolveBidderBid(Bid bid, String currency, List imps, List errors) { + private static BidderBid resolveBidderBid(Bid bid, String currency, List errors) { final BidType bidType; try { - bidType = getBidType(bid.getImpid(), imps); + bidType = getBidType(bid); } catch (PreBidException e) { errors.add(BidderError.badServerResponse(e.getMessage())); return null; @@ -159,22 +157,19 @@ private static BidderBid resolveBidderBid(Bid bid, String currency, List im return BidderBid.of(bid, bidType, currency); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getBanner() != null) { - return BidType.banner; - } - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - throw new PreBidException("Unknown impression type for ID: '%s'".formatted(impId)); - } + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); } - throw new PreBidException("Failed to find impression for ID: '%s'".formatted(impId)); + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid()); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java b/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java index cf0278d4219..19e077624c6 100644 --- a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java +++ b/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java @@ -9,6 +9,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -33,8 +34,10 @@ public class BizzclickBidder implements Bidder { private static final TypeReference> BIZZCLICK_EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String URL_SOURCE_ID_MACRO = "{{.SourceId}}"; - private static final String URL_ACCOUNT_ID_MACRO = "{{.AccountID}}"; + private static final String DEFAULT_HOST = "us-e-node1"; + private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; + private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String DEFAULT_CURRENCY = "USD"; private final String endpointUrl; @@ -100,7 +103,11 @@ private static MultiMap headers(Device device) { } private String buildEndpointUrl(ExtImpBizzclick ext) { - return endpointUrl.replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(ext.getPlacementId())) + final String host = StringUtils.isBlank(ext.getHost()) ? DEFAULT_HOST : ext.getHost(); + final String sourceId = StringUtils.isBlank(ext.getSourceId()) ? ext.getPlacementId() : ext.getSourceId(); + return endpointUrl + .replace(URL_HOST_MACRO, HttpUtil.encodeUrl(host)) + .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(sourceId)) .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(ext.getAccountId())); } diff --git a/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java b/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java index 7785cd476b8..c6f473bc52a 100644 --- a/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java +++ b/src/main/java/org/prebid/server/bidder/iqzone/IqzoneBidder.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.TextNode; 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.apache.commons.collections4.CollectionUtils; @@ -49,32 +50,43 @@ public Result>> makeHttpRequests(BidRequest request final List> httpRequests = new ArrayList<>(); for (Imp imp : request.getImp()) { + final ExtImpIqzone extImpIqzone; try { - final ExtImpIqzone extImpIqzone = parseImpExt(imp); - final Imp modifiedImp = modifyImp(imp, extImpIqzone); - - httpRequests.add(makeHttpRequest(request, modifiedImp)); + extImpIqzone = parseImpExt(imp); } catch (IllegalArgumentException e) { return Result.withError(BidderError.badInput(e.getMessage())); } + + final Imp modifiedImp = modifyImp(imp, extImpIqzone); + httpRequests.add(makeHttpRequest(request, modifiedImp)); } return Result.withValues(httpRequests); } private ExtImpIqzone parseImpExt(Imp imp) { - return mapper.mapper().convertValue(imp.getExt(), IQZONE_EXT_TYPE_REFERENCE).getBidder(); + try { + return mapper.mapper().convertValue(imp.getExt(), IQZONE_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } } private Imp modifyImp(Imp imp, ExtImpIqzone impExt) { final String placementId = impExt.getPlacementId(); - final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + final String endpointId = impExt.getEndpointId(); + + final boolean isPlacementIdEmpty = StringUtils.isEmpty(placementId); + if (isPlacementIdEmpty && StringUtils.isEmpty(endpointId)) { + return imp; + } - if (StringUtils.isNotEmpty(placementId)) { + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + if (!isPlacementIdEmpty) { modifiedImpExtBidder.set("placementId", TextNode.valueOf(placementId)); modifiedImpExtBidder.set("type", TextNode.valueOf("publisher")); } else { - modifiedImpExtBidder.set("endpointId", TextNode.valueOf(impExt.getEndpointId())); + modifiedImpExtBidder.set("endpointId", TextNode.valueOf(endpointId)); modifiedImpExtBidder.set("type", TextNode.valueOf("network")); } @@ -84,8 +96,7 @@ private Imp modifyImp(Imp imp, ExtImpIqzone impExt) { } private HttpRequest makeHttpRequest(BidRequest request, Imp imp) { - final BidRequest outgoingRequest = request.toBuilder().imp(List.of(imp)).build(); - + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); } @@ -93,45 +104,39 @@ private HttpRequest makeHttpRequest(BidRequest request, Imp imp) { public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.withValues(extractBids(bidResponse)); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); - } - - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidMediaType(bid), bidResponse.getCur())) .toList(); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getBanner() != null) { - return BidType.banner; - } - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - throw new PreBidException("Unknown impression type for ID: \"%s\"".formatted(impId)); - } + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); } - throw new PreBidException("Failed to find impression for ID: \"%s\"".formatted(impId)); + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid()); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java b/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java index 9f6181bcdf8..2e88d1becd7 100644 --- a/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java +++ b/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java @@ -2,6 +2,7 @@ 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.apache.commons.collections4.CollectionUtils; @@ -11,12 +12,14 @@ 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.response.BidType; 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; @@ -39,15 +42,21 @@ public Result>> makeHttpRequests(BidRequest bidRequ @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } + + final List errors = new ArrayList<>(); + final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); + + return Result.of(bids, errors); } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse, + List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -59,11 +68,41 @@ private static List extractBids(BidRequest bidRequest, BidResponse bi .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), currency)) + .map(bid -> makeBidderBid(bid, bidRequest.getImp(), currency, errors)) + .filter(Objects::nonNull) .toList(); } - private static BidType resolveBidType(String impId, List imps) { + private static BidType resolveBidType(Bid bid, List imps) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + return resolveBidTypeFromImpId(bid.getImpid(), imps); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> + throw new PreBidException("Unable to fetch mediaType: %s" + .formatted(bid.getImpid())); + }; + } + + private static BidderBid makeBidderBid(Bid bid, List imps, String cur, List errors) { + final BidType bidType; + try { + bidType = resolveBidType(bid, imps); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, cur); + } + + private static BidType resolveBidTypeFromImpId(String impId, List imps) { for (Imp imp : imps) { if (Objects.equals(impId, imp.getId())) { if (imp.getBanner() != null) { diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java index 7de057072f3..4ce93dea167 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -214,6 +214,9 @@ private BigDecimal resolveBidFloor(String kadfloor, BigDecimal existingFloor) { } private static BigDecimal parseKadFloor(String kadFloorString) { + if (StringUtils.isBlank(kadFloorString)) { + return null; + } try { return new BigDecimal(StringUtils.trimToEmpty(kadFloorString)); } catch (NumberFormatException e) { diff --git a/src/main/java/org/prebid/server/bidder/smrtconnect/SmrtconnectBidder.java b/src/main/java/org/prebid/server/bidder/smrtconnect/SmrtconnectBidder.java new file mode 100644 index 00000000000..9e7e8f56369 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smrtconnect/SmrtconnectBidder.java @@ -0,0 +1,110 @@ +package org.prebid.server.bidder.smrtconnect; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +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.smrtconnect.ExtImpSmrtconnect; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SmrtconnectBidder implements Bidder { + + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + private static final TypeReference> SMRTCONNECT_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private final String endpointUrl; + private final JacksonMapper mapper; + + public SmrtconnectBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + final Imp firstImp = bidRequest.getImp().get(0); + final ExtImpSmrtconnect extImpSmrtconnect; + + try { + extImpSmrtconnect = mapper.mapper().convertValue(firstImp.getExt(), SMRTCONNECT_EXT_TYPE_REFERENCE) + .getBidder(); + } catch (IllegalArgumentException e) { + return Result.withError(BidderError.badInput("Ext.bidder not provided")); + } + + return Result.withValue( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveEndpoint(extImpSmrtconnect.getSupplyId())) + .headers(HttpUtil.headers()) + .body(mapper.encodeToBytes(bidRequest)) + .impIds(BidderUtil.impIds(bidRequest)) + .payload(bidRequest) + .build()); + } + + private String resolveEndpoint(String supplyId) { + return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(supplyId)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse("Bad Server Response")); + } catch (PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid.getMtype()), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Integer mType) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + + default -> throw new PreBidException("Unsupported mType " + mType); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index bd957cd0ee0..6a547009892 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -14,6 +16,9 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; @@ -23,11 +28,15 @@ 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.bidder.yieldlab.model.YieldlabDigitalServicesActResponse; import org.prebid.server.bidder.yieldlab.model.YieldlabResponse; 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.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -41,13 +50,16 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; public class YieldlabBidder implements Bidder { + private static final Logger logger = LoggerFactory.getLogger(YieldlabBidder.class); private static final TypeReference> YIELDLAB_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -58,6 +70,9 @@ public class YieldlabBidder implements Bidder { private static final String CREATIVE_ID = "%s%s%s"; private static final String AD_SOURCE_BANNER = ""; private static final String AD_SOURCE_URL = "https://ad.yieldlab.net/d/%s/%s/%s?%s"; + private static final String TRANSPARENCY_TEMPLATE = "%s~%s"; + private static final String TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER = "_"; + private static final String TRANSPARENCY_TEMPLATE_DELIMITER = "~~"; private static final String VAST_MARKUP = """ Yieldlab @@ -189,6 +204,8 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { uriBuilder.addParameter("consent", consent); } + extractDsaRequestParamsFromBidRequest(request).forEach(uriBuilder::addParameter); + return uriBuilder.toString(); } @@ -231,6 +248,63 @@ private static String getConsentParameter(User user) { return ObjectUtils.defaultIfNull(consent, ""); } + private static Map extractDsaRequestParamsFromBidRequest(BidRequest request) { + return Optional.ofNullable(request.getRegs()) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .map(YieldlabBidder::extractDsaRequestParamsFromDsaRegsExtension) + .orElse(Collections.emptyMap()); + } + + private static Map extractDsaRequestParamsFromDsaRegsExtension(final ExtRegsDsa dsa) { + final Map dsaRequestParams = new HashMap<>(); + + if (dsa.getDsaRequired() != null) { + dsaRequestParams.put("dsarequired", dsa.getDsaRequired().toString()); + } + + if (dsa.getPubRender() != null) { + dsaRequestParams.put("dsapubrender", dsa.getPubRender().toString()); + } + + if (dsa.getDataToPub() != null) { + dsaRequestParams.put("dsadatatopub", dsa.getDataToPub().toString()); + } + + final List dsaTransparency = dsa.getTransparency(); + if (CollectionUtils.isNotEmpty(dsaTransparency)) { + final String encodedTransparencies = encodeTransparenciesAsString(dsaTransparency); + if (StringUtils.isNotBlank(encodedTransparencies)) { + dsaRequestParams.put("dsatransparency", encodedTransparencies); + } + } + + return dsaRequestParams; + } + + private static String encodeTransparenciesAsString(List transparencies) { + return transparencies.stream() + .filter(YieldlabBidder::isTransparencyValid) + .map(YieldlabBidder::encodeTransparency) + .collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER)); + } + + private static boolean isTransparencyValid(ExtRegsDsaTransparency transparency) { + return StringUtils.isNotBlank(transparency.getDomain()) + && transparency.getDsaParams() != null + && CollectionUtils.isNotEmpty(transparency.getDsaParams()); + } + + private static String encodeTransparency(ExtRegsDsaTransparency transparency) { + return TRANSPARENCY_TEMPLATE.formatted(transparency.getDomain(), + encodeTransparencyParams(transparency.getDsaParams())); + } + + private static String encodeTransparencyParams(List dsaParams) { + return dsaParams.stream().map(Objects::toString).collect(Collectors.joining( + TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER)); + } + private static MultiMap resolveHeaders(Site site, Device device, User user) { final MultiMap headers = MultiMap.caseInsensitiveMultiMap() .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON); @@ -339,7 +413,8 @@ private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, BidReques .dealid(resolveNumberParameter(yieldlabResponse.getPid())) .crid(makeCreativeId(bidRequest, yieldlabResponse, matchedExtImp)) .w(resolveSizeParameter(yieldlabResponse.getAdSize(), true)) - .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)); + .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)) + .ext(resolveExtParameter(yieldlabResponse)); return updatedBid; } @@ -408,4 +483,21 @@ private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, Yi yieldlabResponse.getAdSize(), uriBuilder.toString().replace("?", "")); } + + private ObjectNode resolveExtParameter(YieldlabResponse yieldlabResponse) { + final YieldlabDigitalServicesActResponse dsa = yieldlabResponse.getDsa(); + if (dsa == null) { + return null; + } + final ObjectNode ext = mapper.mapper().createObjectNode(); + final JsonNode dsaNode; + try { + dsaNode = mapper.mapper().valueToTree(dsa); + } catch (IllegalArgumentException e) { + logger.error("Failed to serialize DSA object for adslot {}", yieldlabResponse.getId(), e); + return null; + } + ext.set("dsa", dsaNode); + return ext; + } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java new file mode 100644 index 00000000000..4fd18d813df --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.yieldlab.model; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") +public class YieldlabDigitalServicesActResponse { + + String behalf; + + String paid; + + Integer adrender; + + List transparency; + + @AllArgsConstructor(staticName = "of") + @Value(staticConstructor = "of") + public static class Transparency { + String domain; + List dsaparams; + } +} diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java index 4cd1dbf7294..9a2b54652ad 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java @@ -22,4 +22,6 @@ public class YieldlabResponse { Integer did; String pvid; + + YieldlabDigitalServicesActResponse dsa; } diff --git a/src/main/java/org/prebid/server/bidder/zmaticoo/ZMaticooBidder.java b/src/main/java/org/prebid/server/bidder/zmaticoo/ZMaticooBidder.java new file mode 100644 index 00000000000..028d3dbc6de --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/zmaticoo/ZMaticooBidder.java @@ -0,0 +1,182 @@ +package org.prebid.server.bidder.zmaticoo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +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.model.UpdateResult; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.zmaticoo.ExtImpZMaticoo; +import org.prebid.server.proto.openrtb.ext.response.BidType; +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; + +public class ZMaticooBidder implements Bidder { + + private static final TypeReference> ZMATICOO_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ZMaticooBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List bidderErrors = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + validateImpExt(imp); + modifiedImps.add(modifyImp(imp)); + } catch (PreBidException e) { + bidderErrors.add(BidderError.badInput(e.getMessage())); + } + } + + if (CollectionUtils.isNotEmpty(bidderErrors)) { + return Result.withErrors(bidderErrors); + } + + final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); + return Result.withValue(makeHttpRequest(modifiedRequest)); + } + + private void validateImpExt(Imp imp) { + final ExtImpZMaticoo extImpZMaticoo; + try { + extImpZMaticoo = mapper.mapper().convertValue(imp.getExt(), ZMATICOO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + if (StringUtils.isBlank(extImpZMaticoo.getPubId()) || StringUtils.isBlank(extImpZMaticoo.getZoneId())) { + throw new PreBidException("imp.ext.pubId or imp.ext.zoneId required"); + } + } + + private Imp modifyImp(Imp imp) { + final Native xNative = imp.getXNative(); + if (xNative == null) { + return imp; + } + + final UpdateResult nativeRequest = resolveNativeRequest(xNative.getRequest()); + return nativeRequest.isUpdated() + ? imp.toBuilder() + .xNative(xNative.toBuilder() + .request(nativeRequest.getValue()) + .build()) + .build() + : imp; + } + + private UpdateResult resolveNativeRequest(String nativeRequest) { + final JsonNode nativeRequestNode; + try { + nativeRequestNode = StringUtils.isNotBlank(nativeRequest) + ? mapper.mapper().readTree(nativeRequest) + : mapper.mapper().createObjectNode(); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + + if (nativeRequestNode.has("native")) { + return UpdateResult.unaltered(nativeRequest); + } + + final String updatedNativeRequest = mapper.mapper().createObjectNode() + .putPOJO("native", nativeRequestNode) + .toString(); + + return UpdateResult.updated(updatedNativeRequest); + } + + private HttpRequest makeHttpRequest(BidRequest modifiedRequest) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(modifiedRequest)) + .payload(modifiedRequest) + .body(mapper.encodeToBytes(modifiedRequest)) + .build(); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + try { + final BidType bidType = getBidMediaType(bid); + return BidderBid.of(bid, bidType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidMediaType(Bid bid) { + final int markupType = ObjectUtils.defaultIfNull(bid.getMtype(), 0); + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "unrecognized bid type in response from zmaticoo for bid " + bid.getImpid()); + }; + } + +} diff --git a/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java b/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java index 0adeb6d8904..9b18c8af24d 100644 --- a/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java +++ b/src/main/java/org/prebid/server/cookie/CookieDeprecationService.java @@ -17,7 +17,6 @@ import org.prebid.server.settings.model.AccountPrivacySandboxCookieDeprecationConfig; import org.prebid.server.util.HttpUtil; -import java.util.Objects; import java.util.Optional; public class CookieDeprecationService { @@ -26,20 +25,12 @@ public class CookieDeprecationService { private static final String DEVICE_EXT_COOKIE_DEPRECATION_FIELD_NAME = "cdep"; private static final long DEFAULT_MAX_AGE = 604800L; - private final Account defaultAccount; - - public CookieDeprecationService(Account defaultAccount) { - this.defaultAccount = Objects.requireNonNull(defaultAccount); - } - public PartitionedCookie makeCookie(Account account, RoutingContext routingContext) { - final Account resolvedAccount = account.isEmpty() ? defaultAccount : account; - - if (hasDeprecationCookieInRequest(routingContext) || isCookieDeprecationDisabled(resolvedAccount)) { + if (hasDeprecationCookieInRequest(routingContext) || isCookieDeprecationDisabled(account)) { return null; } - final Long maxAge = getCookieDeprecationConfig(resolvedAccount) + final Long maxAge = getCookieDeprecationConfig(account) .map(AccountPrivacySandboxCookieDeprecationConfig::getTtlSec) .orElse(DEFAULT_MAX_AGE); @@ -61,13 +52,9 @@ public BidRequest updateBidRequestDevice(BidRequest bidRequest, AuctionContext a .get(HttpUtil.SEC_COOKIE_DEPRECATION); final Account account = auctionContext.getAccount(); - final Account resolvedAccount = account.isEmpty() ? defaultAccount : account; final Device device = bidRequest.getDevice(); - if (secCookieDeprecation == null - || containsCookieDeprecation(device) - || isCookieDeprecationDisabled(resolvedAccount)) { - + if (secCookieDeprecation == null || containsCookieDeprecation(device) || isCookieDeprecationDisabled(account)) { return bidRequest; } diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index cc63eb5e927..87f1916c640 100644 --- a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java +++ b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java @@ -26,6 +26,7 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; @@ -272,7 +273,13 @@ private static BigDecimal getConversionRate(Map> return conversionRate; } - return findIntermediateConversionRate(directCurrencyRates, reverseCurrencyRates); + final BigDecimal intermediateConversionRate = findIntermediateConversionRate(directCurrencyRates, + reverseCurrencyRates); + if (intermediateConversionRate != null) { + return intermediateConversionRate; + } + + return findCrossConversionRate(currencyConversionRates, fromCurrency, toCurrency); } /** @@ -286,7 +293,8 @@ private static BigDecimal findReverseConversionRate(Map curr : null; return reverseConversionRate != null - ? BigDecimal.ONE.divide(reverseConversionRate, reverseConversionRate.precision(), + ? BigDecimal.ONE.divide(reverseConversionRate, + getRatePrecision(reverseConversionRate), RoundingMode.HALF_EVEN) : null; } @@ -310,15 +318,38 @@ private static BigDecimal findIntermediateConversionRate(Map final BigDecimal reverseCurrencyRateIntermediate = reverseCurrencyRates.get(sharedCurrency); conversionRate = directCurrencyRateIntermediate.divide(reverseCurrencyRateIntermediate, // chose largest precision among intermediate rates - reverseCurrencyRateIntermediate.compareTo(directCurrencyRateIntermediate) > 0 - ? reverseCurrencyRateIntermediate.precision() - : directCurrencyRateIntermediate.precision(), + getRatePrecision(directCurrencyRateIntermediate, reverseCurrencyRateIntermediate), RoundingMode.HALF_EVEN); } } return conversionRate; } + private static BigDecimal findCrossConversionRate(Map> currencyConversionRates, + String fromCurrency, + String toCurrency) { + for (Map rates : currencyConversionRates.values()) { + final BigDecimal fromRate = rates.get(fromCurrency); + final BigDecimal toRate = rates.get(toCurrency); + if (fromRate != null && toRate != null) { + return toRate.divide(fromRate, + getRatePrecision(fromRate, toRate), + RoundingMode.HALF_EVEN); + } + } + + return null; + } + + private static int getRatePrecision(BigDecimal... rates) { + final int precision = Arrays.stream(rates) + .map(BigDecimal::precision) + .max(Integer::compareTo) + .orElse(DEFAULT_PRICE_PRECISION); + + return Math.max(precision, DEFAULT_PRICE_PRECISION); + } + private boolean isRatesStale() { if (lastUpdated == null) { return false; diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index fd4e4ef9ab5..3d88078c5a8 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -310,11 +310,8 @@ private void periodicFetch(String accountId) { } private Future accountById(String accountId) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture() - : applicationSettings - .getAccountById(accountId, timeoutFactory.create(ACCOUNT_FETCH_TIMEOUT_MS)) - .recover(ignored -> Future.succeededFuture()); + return applicationSettings.getAccountById(accountId, timeoutFactory.create(ACCOUNT_FETCH_TIMEOUT_MS)) + .otherwiseEmpty(); } @Value(staticConstructor = "of") diff --git a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java index d5bbde4b476..f8d5f3baa3c 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java @@ -1,8 +1,8 @@ package org.prebid.server.floors; -import io.vertx.core.Future; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.log.ConditionalLogger; @@ -35,26 +35,16 @@ public class PriceFloorsConfigResolver { private static final int MAX_ENFORCE_RATE_VALUE = 100; private static final long DEFAULT_MAX_AGE_SEC_VALUE = 86400L; - private final Account defaultAccount; private final Metrics metrics; - private final AccountPriceFloorsConfig defaultFloorsConfig; - public PriceFloorsConfigResolver(Account defaultAccount, Metrics metrics) { - this.defaultAccount = Objects.requireNonNull(defaultAccount); - this.defaultFloorsConfig = getFloorsConfig(defaultAccount); + public PriceFloorsConfigResolver(Metrics metrics) { this.metrics = Objects.requireNonNull(metrics); } - private static AccountPriceFloorsConfig getFloorsConfig(Account account) { - final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); - - return ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); - } - - public Future updateFloorsConfig(Account account) { + public Account resolve(Account account, AccountPriceFloorsConfig fallbackPriceFloorConfig) { try { - validatePriceFloorConfig(account, defaultFloorsConfig); - return Future.succeededFuture(account); + validatePriceFloorConfig(account); + return account; } catch (PreBidException e) { final String message = "Account with id '%s' has invalid config: %s" .formatted(account.getId(), e.getMessage()); @@ -65,75 +55,60 @@ public Future updateFloorsConfig(Account account) { conditionalLogger.error(message, 0.01d); } - return Future.succeededFuture(fallbackToDefaultConfig(account)); + return account.toBuilder() + .auction(account.getAuction().toBuilder().priceFloors(fallbackPriceFloorConfig).build()) + .build(); } - private static void validatePriceFloorConfig(Account account, AccountPriceFloorsConfig defaultFloorsConfig) { + private static void validatePriceFloorConfig(Account account) { final AccountPriceFloorsConfig floorsConfig = getFloorsConfig(account); if (floorsConfig == null) { return; } - final Integer accountEnforceRate = floorsConfig.getEnforceFloorsRate(); - final Integer enforceFloorsRate = accountEnforceRate != null - ? accountEnforceRate - : ObjectUtil.getIfNotNull(defaultFloorsConfig, AccountPriceFloorsConfig::getEnforceFloorsRate); - if (enforceFloorsRate != null - && isNotInRange(enforceFloorsRate, MIN_ENFORCE_RATE_VALUE, MAX_ENFORCE_RATE_VALUE)) { - throw new PreBidException( - invalidPriceFloorsPropertyMessage("enforce-floors-rate", enforceFloorsRate)); + + final Integer enforceRate = floorsConfig.getEnforceFloorsRate(); + if (enforceRate != null && isNotInRange(enforceRate, MIN_ENFORCE_RATE_VALUE, MAX_ENFORCE_RATE_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("enforce-floors-rate", enforceRate)); } + final AccountPriceFloorsFetchConfig fetchConfig = ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getFetch); - final AccountPriceFloorsFetchConfig defaultFetchConfig = - ObjectUtil.getIfNotNull(defaultFloorsConfig, AccountPriceFloorsConfig::getFetch); - validatePriceFloorsFetchConfig(fetchConfig, defaultFetchConfig); + validatePriceFloorsFetchConfig(fetchConfig); } - private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig fetchConfig, - AccountPriceFloorsFetchConfig defaultFetchConfig) { + private static AccountPriceFloorsConfig getFloorsConfig(Account account) { + final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); + + return ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); + } + + private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig fetchConfig) { if (fetchConfig == null) { return; } - final Long accountMaxAgeSec = fetchConfig.getMaxAgeSec(); - final Long defaultMaxAgeSec = - ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getMaxAgeSec); - final long maxAgeSec = accountMaxAgeSec != null - ? accountMaxAgeSec - : defaultMaxAgeSec != null ? defaultMaxAgeSec : DEFAULT_MAX_AGE_SEC_VALUE; + final long maxAgeSec = ObjectUtils.defaultIfNull(fetchConfig.getMaxAgeSec(), DEFAULT_MAX_AGE_SEC_VALUE); if (isNotInRange(maxAgeSec, MIN_MAX_AGE_SEC_VALUE, MAX_AGE_SEC_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-age-sec", maxAgeSec)); } - final Long accountPeriodicSec = fetchConfig.getPeriodSec(); - final Long periodicSec = accountPeriodicSec != null - ? accountPeriodicSec - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec); + final Long periodicSec = fetchConfig.getPeriodSec(); if (periodicSec != null && isNotInRange(periodicSec, MIN_PERIODIC_SEC_VALUE, maxAgeSec)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("period-sec", periodicSec)); } - final Long accountTimeout = fetchConfig.getTimeout(); - final Long timeout = accountTimeout != null - ? accountTimeout - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getTimeout); + final Long timeout = fetchConfig.getTimeout(); if (timeout != null && isNotInRange(timeout, MIN_TIMEOUT_MS_VALUE, MAX_TIMEOUT_MS_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("timeout-ms", timeout)); } - final Long accountMaxRules = fetchConfig.getMaxRules(); - final Long maxRules = accountMaxRules != null - ? accountMaxRules - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getMaxRules); + final Long maxRules = fetchConfig.getMaxRules(); if (maxRules != null && isNotInRange(maxRules, MIN_RULES_VALUE, MAX_RULES_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); } - final Long accountMaxFileSize = fetchConfig.getMaxFileSize(); - final Long maxFileSize = accountMaxFileSize != null - ? accountMaxFileSize - : ObjectUtil.getIfNotNull(defaultFetchConfig, AccountPriceFloorsFetchConfig::getMaxFileSize); + final Long maxFileSize = fetchConfig.getMaxFileSize(); if (maxFileSize != null && isNotInRange(maxFileSize, MIN_FILE_SIZE_VALUE, MAX_FILE_SIZE_VALUE)) { throw new PreBidException(invalidPriceFloorsPropertyMessage("max-file-size-kb", maxFileSize)); } @@ -146,14 +121,4 @@ private static boolean isNotInRange(long number, long min, long max) { private static String invalidPriceFloorsPropertyMessage(String property, Object value) { return "Invalid price-floors property '%s', value passed: %s".formatted(property, value); } - - private Account fallbackToDefaultConfig(Account account) { - final AccountAuctionConfig auctionConfig = account.getAuction(); - final AccountPriceFloorsConfig defaultPriceFloorsConfig = - ObjectUtil.getIfNotNull(defaultAccount.getAuction(), AccountAuctionConfig::getPriceFloors); - - return account.toBuilder() - .auction(auctionConfig.toBuilder().priceFloors(defaultPriceFloorsConfig).build()) - .build(); - } } diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 2009743ec8f..c020ea5adde 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -9,7 +9,6 @@ import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.analytics.model.CookieSyncEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; @@ -158,10 +157,7 @@ private Future fillWithAccount(CookieSyncContext cookieSyncCo } private Future accountById(String accountId, Timeout timeout) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture(Account.empty(accountId)) - : applicationSettings.getAccountById(accountId, timeout) - .otherwise(Account.empty(accountId)); + return applicationSettings.getAccountById(accountId, timeout).otherwise(Account.empty(accountId)); } private CookieSyncContext fillWithGppContext(CookieSyncContext cookieSyncContext) { diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 5472e5a36b1..55edbb5bc17 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -184,10 +184,7 @@ private Future toSetuidContext(RoutingContext routingContext) { } private Future accountById(String accountId, Timeout timeout) { - return StringUtils.isBlank(accountId) - ? Future.succeededFuture(Account.empty(accountId)) - : applicationSettings.getAccountById(accountId, timeout) - .otherwise(Account.empty(accountId)); + return applicationSettings.getAccountById(accountId, timeout).otherwise(Account.empty(accountId)); } private SetuidContext fillWithActivityInfrastructure(SetuidContext setuidContext) { diff --git a/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java b/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java index 21126b7df0e..9bbb38699f8 100644 --- a/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java +++ b/src/main/java/org/prebid/server/privacy/ccpa/Ccpa.java @@ -26,6 +26,9 @@ public boolean isNotEmpty() { } public boolean isEnforced() { + if (usPrivacy == null) { + return false; + } try { validateUsPrivacy(usPrivacy); } catch (PreBidException e) { diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 0e3aba69dfa..ee9b13eb4cc 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -8,11 +8,11 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.execution.Timeout; -import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.metric.MetricName; @@ -27,6 +27,7 @@ import org.prebid.server.settings.model.AccountGdprConfig; import org.prebid.server.settings.model.EnabledForRequestType; import org.prebid.server.settings.model.GdprConfig; +import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; import java.util.Collection; @@ -59,7 +60,7 @@ public class TcfDefinerService { private final boolean consentStringMeansInScope; private final Tcf2Service tcf2Service; private final Set eeaCountries; - private final GeoLocationService geoLocationService; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; private final BidderCatalog bidderCatalog; private final IpAddressHelper ipAddressHelper; private final Metrics metrics; @@ -67,7 +68,7 @@ public class TcfDefinerService { public TcfDefinerService(GdprConfig gdprConfig, Set eeaCountries, Tcf2Service tcf2Service, - GeoLocationService geoLocationService, + GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, Metrics metrics) { @@ -78,7 +79,7 @@ public TcfDefinerService(GdprConfig gdprConfig, && BooleanUtils.isTrue(gdprConfig.getConsentStringMeansInScope()); this.tcf2Service = Objects.requireNonNull(tcf2Service); this.eeaCountries = Objects.requireNonNull(eeaCountries); - this.geoLocationService = geoLocationService; + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.metrics = Objects.requireNonNull(metrics); @@ -93,11 +94,12 @@ public Future resolveTcfContext(Privacy privacy, AccountGdprConfig accountGdprConfig, MetricName requestType, RequestLogInfo requestLogInfo, - Timeout timeout) { + Timeout timeout, + GeoInfo geoInfo) { final Future tcfContextFuture = !isGdprEnabled(accountGdprConfig, requestType) ? Future.succeededFuture(TcfContext.empty()) - : prepareTcfContext(privacy, country, ipAddress, requestLogInfo, timeout); + : prepareTcfContext(privacy, country, ipAddress, requestLogInfo, timeout, geoInfo); return tcfContextFuture.map(this::updateTcfGeoMetrics); } @@ -112,7 +114,15 @@ public Future resolveTcfContext(Privacy privacy, RequestLogInfo requestLogInfo, Timeout timeout) { - return resolveTcfContext(privacy, null, ipAddress, accountGdprConfig, requestType, requestLogInfo, timeout); + return resolveTcfContext( + privacy, + null, + ipAddress, + accountGdprConfig, + requestType, + requestLogInfo, + timeout, + null); } public Future> resultForVendorIds(Set vendorIds, TcfContext tcfContext) { @@ -176,7 +186,8 @@ private Future prepareTcfContext(Privacy privacy, String country, String ipAddress, RequestLogInfo requestLogInfo, - Timeout timeout) { + Timeout timeout, + GeoInfo geoInfo) { final String consentString = privacy.getConsentString(); final TCStringParsingResult consentStringParsingResult = parseConsentString(consentString, requestLogInfo); @@ -205,17 +216,9 @@ private Future prepareTcfContext(Privacy privacy, return Future.succeededFuture(defaultContext.toBuilder().inGdprScope(inScopeOfGdpr(gdpr)).build()); } - if (country != null) { - return Future.succeededFuture(defaultContext.toBuilder().inGdprScope(inScopeOfGdpr(inEea)).build()); - } - - if (ipAddress != null && geoLocationService != null) { - return geoLocationService.lookup(effectiveIpAddress, timeout) - .map(geoInfo -> updateMetricsAndEnrichWithGeo(geoInfo, defaultContext)) - .recover(error -> logError(error, defaultContext)); - } - - return Future.succeededFuture(defaultContext); + return geoLocationServiceWrapper.doLookup(effectiveIpAddress, country, timeout) + .recover(ignored -> Future.succeededFuture(geoInfo)) + .map(lookupResult -> enrichWithGeoInfo(defaultContext, lookupResult, country)); } private String maybeMaskIp(String ipAddress, TCString consent) { @@ -237,28 +240,18 @@ private static boolean shouldMaskIp(TCString consent) { return isConsentValid(consent) && consent.getVersion() == 2 && !consent.getSpecialFeatureOptIns().contains(1); } - private TcfContext updateMetricsAndEnrichWithGeo(GeoInfo geoInfo, TcfContext tcfContext) { - metrics.updateGeoLocationMetric(true); - final Boolean inEea = isCountryInEea(geoInfo.getCountry()); + private TcfContext enrichWithGeoInfo(TcfContext defaultTcfContext, GeoInfo geoInfo, String defaultCountry) { + final String country = ObjectUtil.getIfNotNullOrDefault(geoInfo, GeoInfo::getCountry, () -> defaultCountry); + final Boolean inEea = isCountryInEea(country); final boolean inScope = inScopeOfGdpr(inEea); - return tcfContext.toBuilder() - .geoInfo(geoInfo) - .inGdprScope(inScope) + return defaultTcfContext.toBuilder() .inEea(inEea) + .inGdprScope(inScope) + .geoInfo(geoInfo) .build(); } - private Future logError(Throwable error, TcfContext tcfContext) { - final String message = "Geolocation lookup failed: " + error.getMessage(); - logger.warn(message); - logger.debug(message, error); - - metrics.updateGeoLocationMetric(false); - - return Future.succeededFuture(tcfContext); - } - private Boolean isCountryInEea(String country) { return country != null ? eeaCountries.contains(country) : null; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java index 588137321e3..dae1cd49c62 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java @@ -6,9 +6,15 @@ @Value(staticConstructor = "of") public class ExtImpBizzclick { + @JsonProperty("host") + String host; + @JsonProperty("accountId") String accountId; @JsonProperty("placementId") String placementId; + + @JsonProperty("sourceId") + String sourceId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smrtconnect/ExtImpSmrtconnect.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smrtconnect/ExtImpSmrtconnect.java new file mode 100644 index 00000000000..7dda6c6fa7b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smrtconnect/ExtImpSmrtconnect.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.smrtconnect; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSmrtconnect { + + String supplyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zmaticoo/ExtImpZMaticoo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zmaticoo/ExtImpZMaticoo.java new file mode 100644 index 00000000000..321a136d0ac --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zmaticoo/ExtImpZMaticoo.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.zmaticoo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpZMaticoo { + + @JsonProperty("pubId") + String pubId; + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 297c19ac2af..3b71936c7cd 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -72,7 +72,7 @@ public Future getAccountById(String accountId, Timeout timeout) { return getFromCacheOrDelegate( accountCache, accountToErrorCache, - accountId, + StringUtils.isBlank(accountId) ? StringUtils.EMPTY : accountId, timeout, delegate::getAccountById, event -> metrics.updateSettingsCacheEventMetric(MetricName.account, event)); diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 403583ad70f..11a0d2cb3af 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -1,56 +1,79 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.activity.utils.AccountActivitiesConfigurationUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; +import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.floors.PriceFloorsConfigResolver; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; -import org.prebid.server.log.ConditionalLogger; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountPrivacyConfig; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountPriceFloorsConfig; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class EnrichingApplicationSettings implements ApplicationSettings { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(EnrichingApplicationSettings.class)); - private final boolean enforceValidAccount; - private final double logSamplingRate; private final ApplicationSettings delegate; private final PriceFloorsConfigResolver priceFloorsConfigResolver; + private final ActivitiesConfigResolver activitiesConfigResolver; private final JsonMerger jsonMerger; - private final Account defaultAccount; public EnrichingApplicationSettings(boolean enforceValidAccount, - double logSamplingRate, - Account defaultAccount, + String defaultAccountConfig, ApplicationSettings delegate, PriceFloorsConfigResolver priceFloorsConfigResolver, - JsonMerger jsonMerger) { + ActivitiesConfigResolver activitiesConfigResolver, + JsonMerger jsonMerger, + JacksonMapper mapper) { this.enforceValidAccount = enforceValidAccount; - this.logSamplingRate = logSamplingRate; + this.activitiesConfigResolver = Objects.requireNonNull(activitiesConfigResolver); + this.priceFloorsConfigResolver = Objects.requireNonNull(priceFloorsConfigResolver); this.delegate = Objects.requireNonNull(delegate); this.jsonMerger = Objects.requireNonNull(jsonMerger); - this.priceFloorsConfigResolver = Objects.requireNonNull(priceFloorsConfigResolver); - this.defaultAccount = Objects.requireNonNull(defaultAccount); + + this.defaultAccount = parseAccount(defaultAccountConfig, mapper); } @Override public Future getAccountById(String accountId, Timeout timeout) { - return delegate.getAccountById(accountId, timeout) - .compose(priceFloorsConfigResolver::updateFloorsConfig) - .map(this::mergeAccounts) - .map(this::validateAndModifyAccount) - .recover(throwable -> recoverIfNeeded(throwable, accountId)); + if (StringUtils.isNotBlank(accountId)) { + return delegate.getAccountById(accountId, timeout) + .map(this::mergeAccounts) + .map(account -> priceFloorsConfigResolver.resolve(account, extractDefaultPriceFloors())) + .map(activitiesConfigResolver::resolve) + .recover(throwable -> recoverIfNeeded(throwable, accountId)); + } + + return recoverIfNeeded(new PreBidException("Unauthorized account: account id is empty"), StringUtils.EMPTY); + } + + private AccountPriceFloorsConfig extractDefaultPriceFloors() { + return Optional.ofNullable(defaultAccount) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceFloors) + .orElse(null); + } + + private static Account parseAccount(String accountConfig, JacksonMapper mapper) { + try { + return StringUtils.isNotBlank(accountConfig) + ? mapper.decodeValue(accountConfig, Account.class) + : null; + } catch (DecodeException e) { + throw new IllegalArgumentException("Could not parse default account configuration", e); + } } @Override @@ -91,32 +114,15 @@ public Future getVideoStoredData(String accountId, } private Account mergeAccounts(Account account) { - return jsonMerger.merge(account, defaultAccount, Account.class); - } - - private Account validateAndModifyAccount(Account account) { - if (AccountActivitiesConfigurationUtils.isInvalidActivitiesConfiguration(account)) { - conditionalLogger.warn( - "Activity configuration for account %s contains conditional rule with empty array." - .formatted(account.getId()), - logSamplingRate); - - final AccountPrivacyConfig accountPrivacyConfig = account.getPrivacy(); - return account.toBuilder() - .privacy(accountPrivacyConfig.toBuilder() - .activities(AccountActivitiesConfigurationUtils - .removeInvalidRules(accountPrivacyConfig.getActivities())) - .build()) - .build(); - } - - return account; + return defaultAccount == null + ? account + : jsonMerger.merge(account, defaultAccount, Account.class); } private Future recoverIfNeeded(Throwable throwable, String accountId) { // In case of invalid account return failed future - return !enforceValidAccount - ? Future.succeededFuture(mergeAccounts(Account.empty(accountId))) - : Future.failedFuture(throwable); + return enforceValidAccount + ? Future.failedFuture(throwable) + : Future.succeededFuture(mergeAccounts(Account.empty(accountId))); } } diff --git a/src/main/java/org/prebid/server/settings/model/Account.java b/src/main/java/org/prebid/server/settings/model/Account.java index b26969b87e0..6a71b39f972 100644 --- a/src/main/java/org/prebid/server/settings/model/Account.java +++ b/src/main/java/org/prebid/server/settings/model/Account.java @@ -1,6 +1,5 @@ package org.prebid.server.settings.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; @@ -26,12 +25,10 @@ public class Account { AccountHooksConfiguration hooks; + AccountSettings settings; + public static Account empty(String id) { return Account.builder().id(id).build(); } - @JsonIgnore - public boolean isEmpty() { - return this.equals(empty(id)); - } } diff --git a/src/main/java/org/prebid/server/settings/model/AccountSettings.java b/src/main/java/org/prebid/server/settings/model/AccountSettings.java new file mode 100644 index 00000000000..d2a5bc611d8 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountSettings.java @@ -0,0 +1,12 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountSettings { + + @JsonProperty("geo-lookup") + Boolean geoLookup; + +} diff --git a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java index 9d3c4e62a4b..05f510983b8 100644 --- a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java @@ -4,6 +4,8 @@ import io.vertx.core.http.HttpClientOptions; import lombok.Data; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; import org.prebid.server.execution.RemoteFileSyncer; import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; import org.prebid.server.geolocation.CircuitBreakerSecuredGeoLocationService; @@ -15,6 +17,7 @@ import org.prebid.server.spring.config.model.CircuitBreakerProperties; import org.prebid.server.spring.config.model.HttpClientProperties; import org.prebid.server.spring.config.model.RemoteFileSyncerProperties; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -34,6 +37,7 @@ import java.util.ArrayList; import java.util.List; +@Configuration public class GeoLocationConfiguration { @Configuration @@ -180,22 +184,31 @@ static class GeoInfo { } } - @Configuration - static class CountryCodeMapperConfiguration { + @Bean + public CountryCodeMapper countryCodeMapper(@Value("classpath:country-codes.csv") Resource countryCodes, + @Value("classpath:mcc-country-codes.csv") Resource mccCountryCodes) + throws IOException { - @Bean - public CountryCodeMapper countryCodeMapper(@Value("classpath:country-codes.csv") Resource countryCodes, - @Value("classpath:mcc-country-codes.csv") Resource mccCountryCodes) - throws IOException { + return new CountryCodeMapper(readCsv(countryCodes), readCsv(mccCountryCodes)); + } - return new CountryCodeMapper(readCsv(countryCodes), readCsv(mccCountryCodes)); - } + private String readCsv(Resource resource) throws IOException { + final Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8); + final String csv = FileCopyUtils.copyToString(reader); + reader.close(); + return csv; + } - private String readCsv(Resource resource) throws IOException { - final Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8); - final String csv = FileCopyUtils.copyToString(reader); - reader.close(); - return csv; - } + @Bean + GeoLocationServiceWrapper geoLocationServiceWrapper( + @Autowired(required = false) GeoLocationService geoLocationService, + Ortb2ImplicitParametersResolver implicitParametersResolver, + Metrics metrics) { + + return new GeoLocationServiceWrapper( + geoLocationService, + implicitParametersResolver, + metrics); } + } diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java index 1a6a78a5c42..61df84ee7bf 100644 --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java @@ -3,6 +3,7 @@ import io.vertx.core.Vertx; import io.vertx.core.file.FileSystem; import lombok.Data; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.privacy.enforcement.ActivityEnforcement; import org.prebid.server.auction.privacy.enforcement.CcpaEnforcement; @@ -13,7 +14,6 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdTcfMask; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.HostVendorTcfDefinerService; @@ -46,7 +46,6 @@ import org.prebid.server.settings.model.SpecialFeatures; import org.prebid.server.spring.config.retry.RetryPolicyConfigurationProperties; import org.prebid.server.vertx.http.HttpClient; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -162,7 +161,7 @@ TcfDefinerService tcfDefinerService( GdprConfig gdprConfig, @Value("${gdpr.eea-countries}") String eeaCountriesAsString, Tcf2Service tcf2Service, - @Autowired(required = false) GeoLocationService geoLocationService, + GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, Metrics metrics) { @@ -173,7 +172,7 @@ TcfDefinerService tcfDefinerService( gdprConfig, eeaCountries, tcf2Service, - geoLocationService, + geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, metrics); diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 77f06b2ce38..1180f622661 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -10,6 +10,7 @@ import io.vertx.core.net.JksOptions; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.BidResponseCreator; @@ -18,6 +19,7 @@ import org.prebid.server.auction.DsaEnforcer; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.FpdResolver; +import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.IpAddressHelper; @@ -88,7 +90,6 @@ import org.prebid.server.identity.IdGenerator; import org.prebid.server.identity.NoneIdGenerator; import org.prebid.server.identity.UUIDIdGenerator; -import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.log.CriteriaLogManager; @@ -101,7 +102,6 @@ import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.BidValidationEnforcement; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties; @@ -355,7 +355,6 @@ SetuidGppService setuidGppService(GppService gppService) { @Bean Ortb2RequestFactory openRtb2RequestFactory( - @Value("${settings.enforce-valid-account}") boolean enforceValidAccount, @Value("${auction.biddertmax.percent}") int timeoutAdjustmentFactor, @Value("${auction.blacklisted-accounts}") String blacklistedAccountsString, UidsCookieService uidsCookieService, @@ -374,7 +373,6 @@ Ortb2RequestFactory openRtb2RequestFactory( final List blacklistedAccounts = splitToList(blacklistedAccountsString); return new Ortb2RequestFactory( - enforceValidAccount, timeoutAdjustmentFactor, logSamplingRate, blacklistedAccounts, @@ -405,7 +403,8 @@ AuctionRequestFactory auctionRequestFactory( OrtbTypesResolver ortbTypesResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new AuctionRequestFactory( maxRequestSize, @@ -420,7 +419,8 @@ AuctionRequestFactory auctionRequestFactory( ortbTypesResolver, auctionPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -451,7 +451,8 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, FpdResolver fpdResolver, AmpPrivacyContextFactory ampPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new AmpRequestFactory( ortb2RequestFactory, @@ -464,7 +465,8 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, fpdResolver, ampPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -478,7 +480,8 @@ VideoRequestFactory videoRequestFactory( Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new VideoRequestFactory( maxRequestSize, @@ -490,7 +493,8 @@ VideoRequestFactory videoRequestFactory( ortb2ImplicitParametersResolver, auctionPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -690,9 +694,8 @@ CookieSyncService cookieSyncService( } @Bean - CookieDeprecationService deprecationCookieResolver(Account defaultAccount) { - - return new CookieDeprecationService(defaultAccount); + CookieDeprecationService cookieDeprecationService() { + return new CookieDeprecationService(); } @Bean @@ -990,8 +993,13 @@ RequestValidator requestValidator( } @Bean - PriceFloorsConfigResolver accountValidator(Account defaultAccount, Metrics metrics) { - return new PriceFloorsConfigResolver(defaultAccount, metrics); + PriceFloorsConfigResolver priceFloorsConfigResolver(Metrics metrics) { + return new PriceFloorsConfigResolver(metrics); + } + + @Bean + ActivitiesConfigResolver activitiesConfigResolver(@Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + return new ActivitiesConfigResolver(logSamplingRate); } @Bean @@ -1113,20 +1121,6 @@ DsaEnforcer dsaEnforcer() { return new DsaEnforcer(); } - @Bean - Account defaultAccount(@Value("${settings.default-account-config:#{null}}") String defaultAccountConfig, - JacksonMapper mapper) { - try { - final Account account = StringUtils.isNotBlank(defaultAccountConfig) - ? mapper.decodeValue(defaultAccountConfig, Account.class) - : null; - return account != null ? account : Account.builder().build(); - } catch (DecodeException e) { - logger.warn("Could not parse default account configuration", e); - return Account.builder().build(); - } - } - private static List splitToList(String listAsString) { return splitToCollection(listAsString, ArrayList::new); } diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 25edba67976..4a3ce154ab4 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.UtilityClass; import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JacksonMapper; @@ -20,7 +21,6 @@ import org.prebid.server.settings.HttpApplicationSettings; import org.prebid.server.settings.JdbcApplicationSettings; import org.prebid.server.settings.SettingsCache; -import org.prebid.server.settings.model.Account; import org.prebid.server.settings.service.HttpPeriodicRefreshService; import org.prebid.server.settings.service.JdbcPeriodicRefreshService; import org.prebid.server.spring.config.database.DatabaseConfiguration; @@ -243,19 +243,21 @@ static class EnrichingSettingsConfiguration { @Bean EnrichingApplicationSettings enrichingApplicationSettings( @Value("${settings.enforce-valid-account}") boolean enforceValidAccount, - @Value("${logging.sampling-rate:0.01}") double logSamplingRate, - Account defaultAccount, + @Value("${settings.default-account-config:#{null}}") String defaultAccountConfig, + JacksonMapper mapper, CompositeApplicationSettings compositeApplicationSettings, PriceFloorsConfigResolver priceFloorsConfigResolver, + ActivitiesConfigResolver activitiesConfigResolver, JsonMerger jsonMerger) { return new EnrichingApplicationSettings( enforceValidAccount, - logSamplingRate, - defaultAccount, + defaultAccountConfig, compositeApplicationSettings, priceFloorsConfigResolver, - jsonMerger); + activitiesConfigResolver, + jsonMerger, + mapper); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmrtconnectConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmrtconnectConfiguration.java new file mode 100644 index 00000000000..24eb4d44996 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmrtconnectConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.smrtconnect.SmrtconnectBidder; +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.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 javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/smrtconnect.yaml", factory = YamlPropertySourceFactory.class) +public class SmrtconnectConfiguration { + + private static final String BIDDER_NAME = "smrtconnect"; + + @Bean("smrtconnectConfigurationProperties") + @ConfigurationProperties("adapters.smrtconnect") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps smrtconnectBidderDeps(BidderConfigurationProperties smrtconnectConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(smrtconnectConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SmrtconnectBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZMaticooBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZMaticooBidderConfiguration.java new file mode 100644 index 00000000000..540a58a67ec --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ZMaticooBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.zmaticoo.ZMaticooBidder; +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.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 javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/zmaticoo.yaml", factory = YamlPropertySourceFactory.class) +public class ZMaticooBidderConfiguration { + + private static final String BIDDER_NAME = "zmaticoo"; + + @Bean("zmaticooConfigurationProperties") + @ConfigurationProperties("adapters.zmaticoo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps zmaticooBidderDeps(BidderConfigurationProperties zmaticooConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(zmaticooConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ZMaticooBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/vast/VastModifier.java b/src/main/java/org/prebid/server/vast/VastModifier.java index eb87ab342ca..51721bf9627 100644 --- a/src/main/java/org/prebid/server/vast/VastModifier.java +++ b/src/main/java/org/prebid/server/vast/VastModifier.java @@ -14,15 +14,24 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class VastModifier { - private static final String IN_LINE_TAG = ""; - private static final String IN_LINE_CLOSE_TAG = ""; - private static final String WRAPPER_TAG = ""; - private static final String WRAPPER_CLOSE_TAG = ""; - private static final String IMPRESSION_CLOSE_TAG = ""; + private static final Pattern WRAPPER_OPEN_TAG_PATTERN = + Pattern.compile("<\\s*wrapper(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern WRAPPER_CLOSE_TAG_PATTERN = + Pattern.compile("<\\s*/\\s*wrapper(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern INLINE_OPEN_TAG_PATTERN = + Pattern.compile("<\\s*inline(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern INLINE_CLOSE_TAG_PATTERN = + Pattern.compile("<\\s*/\\s*inline(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private static final Pattern IMPRESSION_CLOSE_TAG_PATTERN = + Pattern.compile("<\\s*/\\s*impression(?:>|\\s.*?>)", Pattern.CASE_INSENSITIVE); + private final BidderCatalog bidderCatalog; private final EventsService eventsService; private final Metrics metrics; @@ -100,45 +109,42 @@ private static String resolveVastXmlFrom(String bidAdm, String bidNurl) { : bidAdm; } - private String appendTrackingUrlToVastXml(String vastXml, String vastUrlTracking, String bidder) { - final int inLineTagIndex = StringUtils.indexOfIgnoreCase(vastXml, IN_LINE_TAG); - final int wrapperTagIndex = StringUtils.indexOfIgnoreCase(vastXml, WRAPPER_TAG); + private static String appendTrackingUrlToVastXml(String xml, String urlTracking, String bidder) { + return appendTrackingUrl(xml, urlTracking, INLINE_OPEN_TAG_PATTERN, INLINE_CLOSE_TAG_PATTERN) + .or(() -> appendTrackingUrl(xml, urlTracking, WRAPPER_OPEN_TAG_PATTERN, WRAPPER_CLOSE_TAG_PATTERN)) + .orElseThrow(() -> new PreBidException( + "VastXml does not contain neither InLine nor Wrapper for %s response".formatted(bidder))); + } + + private static Optional appendTrackingUrl(String vastXml, + String vastUrlTracking, + Pattern openTagPattern, + Pattern closeTagPattern) { - if (inLineTagIndex != -1) { - return appendTrackingUrl(vastXml, vastUrlTracking, IN_LINE_CLOSE_TAG); - } else if (wrapperTagIndex != -1) { - return appendTrackingUrl(vastXml, vastUrlTracking, WRAPPER_CLOSE_TAG); + final Matcher openTagMatcher = openTagPattern.matcher(vastXml); + if (!openTagMatcher.find()) { + return Optional.empty(); } - throw new PreBidException("VastXml does not contain neither InLine nor Wrapper for %s response" - .formatted(bidder)); - } - private static String appendTrackingUrl(String vastXml, String vastUrlTracking, String elementCloseTag) { - if (vastXml.contains(IMPRESSION_CLOSE_TAG)) { - return insertAfterExistingImpressionTag(vastXml, vastUrlTracking); + final Matcher impressionCloseTagMatcher = IMPRESSION_CLOSE_TAG_PATTERN.matcher(vastXml); + if (impressionCloseTagMatcher.find(openTagMatcher.end())) { + int replacementEnd = impressionCloseTagMatcher.end(); + while (impressionCloseTagMatcher.find(replacementEnd)) { + replacementEnd = impressionCloseTagMatcher.end(); + } + return Optional.of(insertUrlTracking(vastXml, replacementEnd, vastUrlTracking)); } - return insertBeforeElementCloseTag(vastXml, vastUrlTracking, elementCloseTag); - } - private static String insertAfterExistingImpressionTag(String vastXml, String vastUrlTracking) { - final String impressionTag = ""; - final int replacementStart = vastXml.lastIndexOf(IMPRESSION_CLOSE_TAG); + final Matcher closeTagMatcher = closeTagPattern.matcher(vastXml); + if (!closeTagMatcher.find(openTagMatcher.end())) { + return Optional.of(vastXml); + } - return vastXml.substring(0, replacementStart) + IMPRESSION_CLOSE_TAG + impressionTag - + vastXml.substring(replacementStart + IMPRESSION_CLOSE_TAG.length()); + return Optional.of(insertUrlTracking(vastXml, closeTagMatcher.start(), vastUrlTracking)); } - private static String insertBeforeElementCloseTag(String vastXml, String vastUrlTracking, String elementCloseTag) { - final int indexOfCloseTag = StringUtils.indexOfIgnoreCase(vastXml, elementCloseTag); - - if (indexOfCloseTag == -1) { - return vastXml; - } - - final String caseSpecificCloseTag = - vastXml.substring(indexOfCloseTag, indexOfCloseTag + elementCloseTag.length()); + private static String insertUrlTracking(String vastXml, int index, String vastUrlTracking) { final String impressionTag = ""; - - return vastXml.replace(caseSpecificCloseTag, impressionTag + caseSpecificCloseTag); + return vastXml.substring(0, index) + impressionTag + vastXml.substring(index); } } diff --git a/src/main/resources/bidder-config/adf.yaml b/src/main/resources/bidder-config/adf.yaml index ef554038170..738b737c11c 100644 --- a/src/main/resources/bidder-config/adf.yaml +++ b/src/main/resources/bidder-config/adf.yaml @@ -18,6 +18,6 @@ adapters: usersync: cookie-family-name: adf redirect: - url: https://cm.adform.net/cookie?redirect_url={{redirect_url}} + url: https://c1.adform.net/cookie?redirect_url={{redirect_url}} support-cors: false uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/bizzclick.yaml b/src/main/resources/bidder-config/bizzclick.yaml index afb208477ae..f5037c1014a 100644 --- a/src/main/resources/bidder-config/bizzclick.yaml +++ b/src/main/resources/bidder-config/bizzclick.yaml @@ -1,6 +1,6 @@ adapters: bizzclick: - endpoint: http://us-e-node1.bizzclick.com/bid?rtb_seat_id={{.SourceId}}&secret_key={{.AccountID}} + endpoint: http://{{Host}}.bizzclick.com/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} meta-info: maintainer-email: support@bizzclick.com app-media-types: diff --git a/src/main/resources/bidder-config/grid.yaml b/src/main/resources/bidder-config/grid.yaml index b9365394326..e735a5159f7 100644 --- a/src/main/resources/bidder-config/grid.yaml +++ b/src/main/resources/bidder-config/grid.yaml @@ -6,14 +6,16 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 686 usersync: cookie-family-name: grid redirect: - url: https://x.bidswitch.net/check_uuid/{{redirect_url}} + url: https://x.bidswitch.net/check_uuid/{{redirect_url}}?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&us_privacy={{us_privacy}} support-cors: false uid-macro: '${BSW_UUID}' diff --git a/src/main/resources/bidder-config/mgidx.yaml b/src/main/resources/bidder-config/mgidx.yaml index 0228aa09cc7..7c1b74aaae3 100644 --- a/src/main/resources/bidder-config/mgidx.yaml +++ b/src/main/resources/bidder-config/mgidx.yaml @@ -1,6 +1,8 @@ adapters: mgidX: - endpoint: https://us-east-x.mgid.com/pserver + # We have the following regional endpoint domains: 'us-east-x' and 'eu' + # Please deploy this config in each of your datacenters with the appropriate regional subdomain + endpoint: https://REGION.mgid.com/pserver meta-info: maintainer-email: prebid@mgid.com app-media-types: diff --git a/src/main/resources/bidder-config/rtbhouse.yaml b/src/main/resources/bidder-config/rtbhouse.yaml index fd2f73da770..774ed55715c 100644 --- a/src/main/resources/bidder-config/rtbhouse.yaml +++ b/src/main/resources/bidder-config/rtbhouse.yaml @@ -1,6 +1,17 @@ adapters: rtbhouse: - endpoint: https://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids + # Contact prebid@rtbhouse.com to ask about enabling a connection to the bidder. + # Please configure the following endpoints for your datacenter + # EMEA + endpoint: http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids + # US East + # endpoint: http://prebidserver-s2s-ash.creativecdn.com/bidder/prebidserver/bids + # US West + # endpoint: http://prebidserver-s2s-phx.creativecdn.com/bidder/prebidserver/bids + # APAC + # endpoint: http://prebidserver-s2s-sin.creativecdn.com/bidder/prebidserver/bids + geoscope: + - global endpoint-compression: gzip meta-info: maintainer-email: prebid@rtbhouse.com diff --git a/src/main/resources/bidder-config/smrtconnect.yaml b/src/main/resources/bidder-config/smrtconnect.yaml new file mode 100644 index 00000000000..c9ee3a640ef --- /dev/null +++ b/src/main/resources/bidder-config/smrtconnect.yaml @@ -0,0 +1,21 @@ +adapters: + smrtconnect: + endpoint: https://amp.smrtconnect.com/openrtb2/auction?supply_id={{SupplyId}} + # This bidder does not operate globally. Please consider setting "disabled: true" in European datacenters. + geoscope: + - "!EEA" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid@smrtconnect.com + app-media-types: + - banner + - native + - video + - audio + site-media-types: + - banner + - native + - video + - audio + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/yieldlab.yaml b/src/main/resources/bidder-config/yieldlab.yaml index 537436ab41d..806d279bc26 100644 --- a/src/main/resources/bidder-config/yieldlab.yaml +++ b/src/main/resources/bidder-config/yieldlab.yaml @@ -1,6 +1,6 @@ adapters: yieldlab: - endpoint: https://ad.yieldlab.net/yp/ + endpoint: https://ad.yieldlab.net/yp meta-info: maintainer-email: solutions@yieldlab.de app-media-types: diff --git a/src/main/resources/bidder-config/zmaticoo.yaml b/src/main/resources/bidder-config/zmaticoo.yaml new file mode 100644 index 00000000000..718fff17436 --- /dev/null +++ b/src/main/resources/bidder-config/zmaticoo.yaml @@ -0,0 +1,12 @@ +adapters: + zmaticoo: + endpoint: https://bid.zmaticoo.com/prebid/bid + meta-info: + maintainer-email: adam.li@eclicktech.com.cn + app-media-types: + - banner + - video + - native + site-media-types: + supported-vendors: + vendor-id: 803 diff --git a/src/main/resources/static/bidder-params/smrtconnect.json b/src/main/resources/static/bidder-params/smrtconnect.json new file mode 100644 index 00000000000..74229a46133 --- /dev/null +++ b/src/main/resources/static/bidder-params/smrtconnect.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smrtconnect Params", + "description": "A schema which validates params accepted by the Smrtconnect", + "type": "object", + "properties": { + "supply_id": { + "type": "string", + "description": "Supply id", + "minLength": 1 + } + }, + "required": ["supply_id"] +} diff --git a/src/main/resources/static/bidder-params/zmaticoo.json b/src/main/resources/static/bidder-params/zmaticoo.json new file mode 100644 index 00000000000..8200c94d13c --- /dev/null +++ b/src/main/resources/static/bidder-params/zmaticoo.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "zMaticoo Adapter Params", + "description": "A schema which validates params accepted by the zMaticoo adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "description": "Publisher ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone Id", + "minLength": 1 + } + }, + "required": [ + "pubId", + "zoneId" + ] +} diff --git a/src/test/groovy/org/prebid/server/functional/model/Currency.groovy b/src/test/groovy/org/prebid/server/functional/model/Currency.groovy index cd3360510fa..fb48fc4a6d1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/Currency.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/Currency.groovy @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum Currency { - USD, EUR, GBP, JPY, BOGUS + USD, EUR, GBP, JPY, CHF, CAD, BOGUS @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy index 08bb7b4d1cf..c7a3c51d6ea 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy @@ -19,6 +19,7 @@ class AccountConfig { AccountMetricsConfig metrics AccountCookieSyncConfig cookieSync AccountHooksConfiguration hooks + AccountSetting settings static getDefaultAccountConfig() { new AccountConfig(status: AccountStatus.ACTIVE) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountSetting.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountSetting.groovy new file mode 100644 index 00000000000..0a056057229 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountSetting.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AccountSetting { + + Boolean geoLookup +} diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy index b1d4368cf75..c9c55c129d7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/Country.groovy @@ -5,23 +5,26 @@ import org.prebid.server.functional.util.privacy.model.State enum Country { - USA("USA"), - CAN("CAN"), - MULTIPLE("*") + USA("USA","US"), + CAN("CAN","CA"), + MULTIPLE("*","*") @JsonValue - final String value + final String ISOAlpha3 - Country(String value) { - this.value = value + final String ISOAlpha2 + + Country(String ISOAlpha3,String ISOAlpha2) { + this.ISOAlpha3 = ISOAlpha3 + this.ISOAlpha2 = ISOAlpha2 } @Override String toString() { - value + ISOAlpha3 } String withState(State state) { - return "${value}.${state.abbreviation}".toString() + return "${ISOAlpha3}.${state.abbreviation}".toString() } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy new file mode 100644 index 00000000000..59fb0b34c25 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.request.auction + +enum PublicCountryIp { + + USA_IP("209.232.44.21", "d646:2414:17b2:f371:9b62:f176:b4c0:51cd"), + UKR_IP("193.238.111.14", "3080:f30f:e4bc:0f56:41be:6aab:9d0a:58e2"), + CAN_IP("70.71.245.39", "f9b2:c742:1922:7d4b:7122:c7fc:8b75:98c8") + + final String v4 + final String v6 + + PublicCountryIp(String v4, String ipV6) { + this.v4 = v4 + this.v6 = ipV6 + } +} diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index f7fd6d418a6..ca9efffb62a 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -1,7 +1,6 @@ package org.prebid.server.functional.service import com.fasterxml.jackson.core.type.TypeReference -import io.qameta.allure.Step import io.restassured.authentication.AuthenticationScheme import io.restassured.authentication.BasicAuthScheme import io.restassured.builder.RequestSpecBuilder @@ -78,7 +77,6 @@ class PrebidServerService implements ObjectMapperWrapper { prometheusRequestSpecification = buildAndGetRequestSpecification(pbsContainer.prometheusRootUri, authenticationScheme) } - @Step("[POST] /openrtb2/auction") BidResponse sendAuctionRequest(BidRequest bidRequest, Map headers = [:]) { def response = postAuction(bidRequest, headers) @@ -86,7 +84,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), BidResponse) } - @Step("[POST RAW] /openrtb2/auction") RawAuctionResponse sendAuctionRequestRaw(BidRequest bidRequest, Map headers = [:]) { def response = postAuction(bidRequest, headers) @@ -96,7 +93,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[GET] /openrtb2/amp") AmpResponse sendAmpRequest(AmpRequest ampRequest, Map headers = [:]) { def response = getAmp(ampRequest, headers) @@ -104,7 +100,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), AmpResponse) } - @Step("[GET RAW] /openrtb2/amp") RawAmpResponse sendAmpRequestRaw(AmpRequest ampRequest, Map headers = [:]) { def response = getAmp(ampRequest, headers) @@ -114,7 +109,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[POST] /cookie_sync without cookie") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request) { def response = postCookieSync(request) @@ -122,7 +116,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST] /cookie_sync with headers") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request, Map headers) { def response = postCookieSync(request, null, headers) @@ -130,7 +123,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST] /cookie_sync with uids cookie") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request, UidsCookie uidsCookie) { def response = postCookieSync(request, uidsCookie) @@ -138,7 +130,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST] /cookie_sync with uids and additional cookies") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request, UidsCookie uidsCookie, Map additionalCookies) { @@ -148,7 +139,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CookieSyncResponse) } - @Step("[POST RAW] /cookie_sync with uids cookies") RawCookieSyncResponse sendCookieSyncRequestRaw(CookieSyncRequest request, UidsCookie uidsCookie) { def response = postCookieSync(request, uidsCookie) @@ -158,7 +148,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[POST RAW] /cookie_sync with uids and additional cookies") RawCookieSyncResponse sendCookieSyncRequestRaw(CookieSyncRequest request, UidsCookie uidsCookie, Map additionalCookies) { @@ -170,7 +159,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[GET] /setuid") SetuidResponse sendSetUidRequest(SetuidRequest request, UidsCookie uidsCookie, Map header = [:]) { def uidsCookieAsJson = encode(uidsCookie) def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) @@ -188,7 +176,6 @@ class PrebidServerService implements ObjectMapperWrapper { setuidResponse } - @Step("[GET] /getuids") GetuidResponse sendGetUidRequest(UidsCookie uidsCookie) { def uidsCookieAsJson = encode(uidsCookie) def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) @@ -200,7 +187,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), GetuidResponse) } - @Step("[GET] /event") byte[] sendEventRequest(EventRequest eventRequest, Map headers = [:]) { def response = given(requestSpecification).headers(headers) .queryParams(toMap(eventRequest)) @@ -210,7 +196,6 @@ class PrebidServerService implements ObjectMapperWrapper { response.body.asByteArray() } - @Step("[POST] /vtrack") PrebidCacheResponse sendVtrackRequest(VtrackRequest request, String account) { def response = given(requestSpecification).queryParam("a", account) .body(request) @@ -220,7 +205,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), PrebidCacheResponse) } - @Step("[GET] /status") StatusResponse sendStatusRequest() { def response = given(requestSpecification).get(STATUS_ENDPOINT) @@ -228,7 +212,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), StatusResponse) } - @Step("[GET] /info/bidders") String sendInfoBiddersRequest() { def response = given(requestSpecification).get(INFO_BIDDERS_ENDPOINT) @@ -236,7 +219,6 @@ class PrebidServerService implements ObjectMapperWrapper { response.body().asString() } - @Step("[GET] /info/bidders with params={queryParam}") List sendInfoBiddersRequest(Map queryParam) { def response = given(requestSpecification).queryParams(queryParam).get(INFO_BIDDERS_ENDPOINT) @@ -244,7 +226,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.asString(), new TypeReference>() {}) } - @Step("[GET] /info/bidders/{bidderName}") BidderInfoResponse sendBidderInfoRequest(BidderName bidderName) { def response = given(requestSpecification).get("$INFO_BIDDERS_ENDPOINT/$bidderName.value") @@ -253,7 +234,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), BidderInfoResponse) } - @Step("[GET] /bidders/params") BiddersParamsResponse sendBiddersParamsRequest() { def response = given(requestSpecification).get(BIDDERS_PARAMS_ENDPOINT) @@ -261,7 +241,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), BiddersParamsResponse) } - @Step("[GET] /currency/rates") CurrencyRatesResponse sendCurrencyRatesRequest() { def response = given(adminRequestSpecification).get(CURRENCY_RATES_ENDPOINT) @@ -269,7 +248,6 @@ class PrebidServerService implements ObjectMapperWrapper { decode(response.body.asString(), CurrencyRatesResponse) } - @Step("[GET] /logging/httpinteraction") String sendLoggingHttpInteractionRequest(HttpInteractionRequest httpInteractionRequest) { def response = given(adminRequestSpecification).queryParams(toMap(httpInteractionRequest)) .get(HTTP_INTERACTION_ENDPOINT) @@ -278,7 +256,6 @@ class PrebidServerService implements ObjectMapperWrapper { response.body().asString() } - @Step("[GET] /collected-metrics") Map sendCollectedMetricsRequest() { def response = given(adminRequestSpecification).get(COLLECTED_METRICS_ENDPOINT) @@ -287,7 +264,6 @@ class PrebidServerService implements ObjectMapperWrapper { } - @Step("[GET] /metrics") String sendPrometheusMetricsRequest() { def response = given(prometheusRequestSpecification).get(PROMETHEUS_METRICS_ENDPOINT) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy index 878944351ff..0dd9412c3cc 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.testcontainers.scaffolding import org.mockserver.matchers.TimeToLive import org.mockserver.matchers.Times +import org.mockserver.model.Delay import org.mockserver.model.HttpRequest import org.mockserver.model.HttpResponse import org.testcontainers.containers.MockServerContainer @@ -17,7 +18,7 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.TcfPolicyVers class VendorList extends NetworkScaffolding { - private static final String VENDOR_LIST_ENDPOINT = "/{TCF_POLICY}/vendor-list.json" + private static final String VENDOR_LIST_ENDPOINT = "/v{TCF_POLICY}/vendor-list.json" VendorList(MockServerContainer mockServerContainer) { super(mockServerContainer, VENDOR_LIST_ENDPOINT) @@ -39,16 +40,19 @@ class VendorList extends NetworkScaffolding { } void setResponse(TcfPolicyVersion tcfPolicyVersion = TCF_POLICY_V2, + Delay delay = null, Map vendors = [(GENERIC_VENDOR_ID): Vendor.getDefaultVendor(GENERIC_VENDOR_ID)]) { - def prepareEndpoint = endpoint.replace("{TCF_POLICY}", "v" + tcfPolicyVersion.vendorListVersion) + def prepareEndpoint = endpoint.replace("{TCF_POLICY}", tcfPolicyVersion.vendorListVersion.toString()) def prepareEncodeResponseBody = encode(defaultVendorListResponse.tap { it.tcfPolicyVersion = tcfPolicyVersion.vendorListVersion it.vendors = vendors }) mockServerClient.when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) - .respond { request -> request.withPath(endpoint) - ? response().withStatusCode(OK_200.code()).withBody(prepareEncodeResponseBody) - : HttpResponse.notFoundResponse()} + .respond { request -> + request.withPath(endpoint) + ? response().withStatusCode(OK_200.code()).withDelay(delay).withBody(prepareEncodeResponseBody) + : HttpResponse.notFoundResponse() + } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index 14467df1967..0e5328dc3ae 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1,6 +1,5 @@ package org.prebid.server.functional.tests -import io.qameta.allure.Issue import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredImp @@ -271,7 +270,6 @@ class BidderParamsSpec extends BaseSpec { } // TODO: create same test for enabled circuit breaker - @Issue("https://github.com/prebid/prebid-server-java/issues/1478") def "PBS should emit warning when bidder endpoint is invalid"() { given: "Pbs config" def pbsService = pbsServiceFactory.getService(["adapters.generic.enabled" : "true", diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index 3f9ccc7ab43..ccd5f8b9cf0 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -1,5 +1,9 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountEventsConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp @@ -192,4 +196,50 @@ class CacheSpec extends BaseSpec { false | BANNER true | VIDEO } + + def "PBS should update prebid_cache.creative_size.xml metric and adding tracking xml when xml creative contain #wrapper and impression are valid xml value"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok") + + and: "Create and save enabled events config in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + uuid = accountId + config = new AccountConfig().tap { + auction = new AccountAuctionConfig(events: new AccountEventsConfig(enabled: true)) + } + } + accountDao.save(account) + + and: "Vtrack request with custom tags" + def payload = PBSUtils.randomString + def creative = "<${wrapper}>prebid.org wrapper" + + "<![CDATA[//${payload}]]>" + + "<${impression}> <![CDATA[ ]]> " + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, accountId) + + then: "Vast xml is modified" + def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) + assert prebidCacheRequest.size() == 1 + assert prebidCacheRequest[0].contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + + and: "prebid_cache.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics["prebid_cache.requests.ok"] == initialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics["account.${accountId}.prebid_cache.requests.ok" as String] == 1 + + where: + wrapper | impression + " wrapper " | " impression " + PBSUtils.getRandomCase(" wrapper ") | PBSUtils.getRandomCase(" impression ") + " wraPPer ${PBSUtils.getRandomString()} " | " imPreSSion ${PBSUtils.getRandomString()}" + " inLine " | " ImpreSSion $PBSUtils.randomNumber" + PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " + " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy index ab22cb65cc2..568f202be55 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -9,6 +9,8 @@ import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversio import java.math.RoundingMode +import static org.prebid.server.functional.model.Currency.CAD +import static org.prebid.server.functional.model.Currency.CHF 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.Currency.USD @@ -18,8 +20,11 @@ class CurrencySpec extends BaseSpec { private static final Currency DEFAULT_CURRENCY = USD private static final int PRICE_PRECISION = 3 - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.8872327211427558, - (JPY): 114.12], + private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(USD): 1, + (EUR): 0.9249838127832763, + (CHF): 0.9033391915641477, + (JPY): 151.1886041994265, + (CAD): 1.357136250115623], (EUR): [(USD): 1.3429368029739777]] private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) @@ -113,6 +118,34 @@ class CurrencySpec extends BaseSpec { JPY || USD } + def "PBS should use cross currency conversion when direct, reverse and intermediate conversion is not available"() { + given: "Default BidRequest with #requestCurrency currency" + def bidRequest = BidRequest.defaultBidRequest.tap { cur = [requestCurrency] } + + and: "Default Bid with a #bidCurrency currency" + def bidderResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = bidCurrency } + bidder.setResponse(bidRequest.id, bidderResponse) + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Auction response should contain bid in #requestCurrency currency" + assert bidResponse.cur == requestCurrency + def bidPrice = bidResponse.seatbid[0].bid[0].price + assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price + assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency + + where: + requestCurrency || bidCurrency + CHF || JPY + JPY || CHF + CAD || JPY + JPY || CAD + EUR || CHF + CHF || EUR + } + private static Map getExternalCurrencyConverterConfig() { ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, "currency-converter.external-rates.enabled" : "true", @@ -129,11 +162,26 @@ class CurrencySpec extends BaseSpec { def conversionRate if (fromCurrency == toCurrency) { conversionRate = 1 - } else if (fromCurrency in DEFAULT_CURRENCY_RATES) { + } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else { + } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + } else { + conversionRate = getCrossConversionRate(fromCurrency, toCurrency) } conversionRate } + + private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { + for (Map rates : DEFAULT_CURRENCY_RATES.values()) { + def fromRate = rates?[fromCurrency] + def toRate = rates?[toCurrency] + + if (fromRate && toRate) { + return toRate / fromRate + } + } + + null + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/GeoSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/GeoSpec.groovy new file mode 100644 index 00000000000..ac5c91cad54 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/GeoSpec.groovy @@ -0,0 +1,496 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.config.AccountSetting +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.util.PBSUtils +import java.time.Instant + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +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.request.auction.PublicCountryIp.CAN_IP +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.USA_IP +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.util.privacy.model.State.ALABAMA +import static org.prebid.server.functional.util.privacy.model.State.ONTARIO +import static org.prebid.server.functional.util.privacy.model.State.QUEBEC + +class GeoSpec extends BaseSpec { + + private static final String GEO_LOCATION_REQUESTS = "geolocation_requests" + private static final String GEO_LOCATION_FAIL = "geolocation_fail" + private static final String GEO_LOCATION_SUCCESSFUL = "geolocation_successful" + private static final Map GEO_LOCATION = ["geolocation.type" : "configuration", + "geolocation.configurations.[0].address-pattern" : USA_IP.v4, + "geolocation.configurations.[0].geo-info.country": USA.ISOAlpha2, + "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation, + "geolocation.configurations.[1].address-pattern" : CAN_IP.v4, + "geolocation.configurations.[1].geo-info.country": CAN.ISOAlpha2, + "geolocation.configurations.[1].geo-info.region" : QUEBEC.abbreviation] + + def "PBS should populate geo with country and region and take precedence from device.id when geo location enabled in host and account config and ip specified in both places"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookup) + } + def defaultPbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(config), + "geolocation.enabled" : "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": CAN_IP.v4]) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == USA + assert bidderRequests.device.geo.region == ALABAMA.abbreviation + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookup | accountGeoLookup + false | true + true | true + true | null + } + + def "PBS should populate geo with country and region when geo location enabled in host and account config and ip present in device.id"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookup) + } + def defaultPbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(config), + "geolocation.enabled" : "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == USA + assert bidderRequests.device.geo.region == ALABAMA.abbreviation + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookup | accountGeoLookup + false | true + true | true + true | null + } + + def "PBS should populate geo with country and region when geo location enabled in host and account config and ip present in header"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookup) + } + def defaultPbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(config), + "geolocation.enabled" : "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == USA + assert bidderRequests.device.geo.region == ALABAMA.abbreviation + + and: "Metrics processed across activities should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookup | accountGeoLookup + false | true + true | true + true | null + } + + def "PBS shouldn't populate geo with country and region when geo location disable in host and account config enabled and ip present in device.ip"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookupConfig) + } + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["settings.default-account-config": encode(config), + "geolocation.enabled" : hostGeolocation]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookupConfig | hostGeolocation | accountGeoLookup + true | "true" | false + true | "false" | true + false | "false" | false + false | "true" | false + } + + def "PBS shouldn't populate geo with country and region when geo location disable in host and account config enabled and ip present in header"() { + given: "PBS service with geolocation and default account configs" + def config = AccountConfig.defaultAccountConfig.tap { + settings = new AccountSetting(geoLookup: defaultAccountGeoLookupConfig) + } + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["settings.default-account-config": encode(config), + "geolocation.enabled" : hostGeolocation]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: accountGeoLookup)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + + where: + defaultAccountGeoLookupConfig | hostGeolocation | accountGeoLookup + true | "true" | false + true | "false" | true + false | "false" | false + false | "true" | false + } + + def "PBS shouldn't populate geo with country, region and emit error in log and metric when geo look up failed and ip present in device.id"() { + given: "Test start time" + def startTime = Instant.now() + + and: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["geolocation.configurations.[0].address-pattern": PBSUtils.randomNumber as String, + "geolocation.enabled" : "true"]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_FAIL] == 1 + assert !metrics[GEO_LOCATION_SUCCESSFUL] + + and: "PBs should emit geo failed logs" + def logs = defaultPbsService.getLogsByTime(startTime) + def getLocation = getLogsByText(logs, "GeoLocationServiceWrapper") + assert getLocation.size() == 1 + assert getLocation[0].contains("Geolocation lookup failed: " + + "ConfigurationGeoLocationService: Geo location lookup failed.") + } + + def "PBS shouldn't populate geo with country, region and emit error in log and metric when geo look up failed and ip present in header"() { + given: "Test start time" + def startTime = Instant.now() + + and: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService(GEO_LOCATION + + ["geolocation.configurations.[0].address-pattern": PBSUtils.randomNumber as String, + "geolocation.enabled" : "true"]) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: null, + region: null, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request shouldn't contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequests.device.geo.country + assert !bidderRequests.device.geo.region + + and: "Metrics processed across geo location should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_FAIL] == 1 + assert !metrics[GEO_LOCATION_SUCCESSFUL] + + and: "PBs should emit geo failed logs" + def logs = defaultPbsService.getLogsByTime(startTime) + def getLocation = getLogsByText(logs, "GeoLocationServiceWrapper") + assert getLocation.size() == 1 + assert getLocation[0].contains("Geolocation lookup failed: " + + "ConfigurationGeoLocationService: Geo location lookup failed.") + } + + def "PBS shouldn't populate country and region via geo when geo enabled in account and country and region specified in request and ip present in device.id"() { + given: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService( + ["geolocation.enabled": "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: USA_IP.v4, + ipv6: USA_IP.v6, + geo: new Geo( + country: CAN, + region: ONTARIO.abbreviation, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == CAN + assert bidderRequests.device.geo.region == ONTARIO.abbreviation + + and: "Metrics processed across activities shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + } + + def "PBS shouldn't populate country and region via geo when geo enabled in account and country and region specified in request and ip present in header"() { + given: "PBS service with geolocation" + def defaultPbsService = pbsServiceFactory.getService( + ["geolocation.enabled": "true"] + GEO_LOCATION) + + and: "Default bid request with device and geo data" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device( + ip: null, + ipv6: null, + geo: new Geo( + country: CAN, + region: ONTARIO.abbreviation, + lat: PBSUtils.getRandomDecimal(0, 90), + lon: PBSUtils.getRandomDecimal(0, 90))) + ext.prebid.trace = VERBOSE + } + + and: "Account in the DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + settings: new AccountSetting(geoLookup: true)) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Flush metric" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Bidder request should contain country and region" + def bidderRequests = bidder.getBidderRequest(bidRequest.id) + assert bidderRequests.device.geo.country == CAN + assert bidderRequests.device.geo.region == ONTARIO.abbreviation + + and: "Metrics processed across activities shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[GEO_LOCATION_REQUESTS] + assert !metrics[GEO_LOCATION_SUCCESSFUL] + assert !metrics[GEO_LOCATION_FAIL] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index cc6832c479d..b22ef5438c6 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -586,8 +586,9 @@ class OrtbConverterSpec extends BaseSpec { } } - def "PBS should remove imp[0].video.* when we don't support ortb 2.6"() { + def "PBS should remove imp[0].video.* and keep imp[0].video.plcmt when we don't support ortb 2.6"() { given: "Default bid request with imp[0].video.*" + def placement = PBSUtils.getRandomEnum(VideoPlcmtSubtype) def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].video = Video.defaultVideo.tap { rqddurs = [PBSUtils.randomNumber] @@ -597,15 +598,16 @@ class OrtbConverterSpec extends BaseSpec { podseq = PBSUtils.randomNumber mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber - plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + plcmt = placement } } when: "Requesting PBS auction with ortb 2.5" prebidServerServiceWithElderOrtb.sendAuctionRequest(bidRequest) - then: "BidResponse shouldn't contain the imp[0].video.* as on request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { + then: "Bidder request shouldn't contain the imp[0].video.* as on request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { !imp[0].video.rqddurs !imp[0].video.maxseq !imp[0].video.poddur @@ -613,8 +615,10 @@ class OrtbConverterSpec extends BaseSpec { !imp[0].video.podseq !imp[0].video.mincpmpersec !imp[0].video.slotinpod - !imp[0].video.plcmt } + + and: "Bidder request should contain the imp[0].video.* as on request" + bidderRequest.imp[0].video.plcmt == placement } def "PBS shouldn't remove imp[0].video.* when we support ortb 2.6"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy index d04808f4fa2..99fa3b31a0e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy @@ -1,11 +1,9 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.PrebidStoredRequest -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.PBSUtils @@ -17,7 +15,6 @@ import static org.prebid.server.functional.testcontainers.container.PrebidServer class TimeoutSpec extends BaseSpec { private static final int DEFAULT_TIMEOUT = getRandomTimeout() - private static final int MIN_TIMEOUT_BIDDER_REQUEST = 5 private static final int MIN_TIMEOUT = PBSUtils.getRandomNumber(50, 150) private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, "auction.biddertmax.min" : MIN_TIMEOUT as String] @@ -284,76 +281,6 @@ class TimeoutSpec extends BaseSpec { assert isInternalProcessingTime(bidderRequest.tmax, MAX_TIMEOUT) } - def "PBS amp should return error when auction.biddertmax.min value not enough for bidder request"() { - given: "PBS config with biddertmax.min" - def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.min" : MIN_TIMEOUT_BIDDER_REQUEST as String]) - - and: "Default AMP request without timeout" - def ampRequest = AmpRequest.defaultAmpRequest.tap { - timeout = null - } - - and: "Default stored request tmax" - def minTmax = MIN_TIMEOUT_BIDDER_REQUEST - 1 - def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - tmax = minTmax - } - - and: "Save storedRequest into DB" - def storedRequestModel = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequestModel) - - when: "PBS processes amp request" - def bidResponse = prebidServerService.sendAmpRequest(ampRequest) - - then: "Bidder request timeout should correspond to the min from the stored request" - assert bidResponse?.ext?.debug?.resolvedRequest?.tmax == minTmax - - and: "PBS should send to bidder tmax form auction.biddertmax.min config" - assert bidResponse.ext.debug.httpcalls[BidderName.GENERIC.value]*.requestBody[0].contains("\"tmax\":${MIN_TIMEOUT_BIDDER_REQUEST}") - - and: "Bid response should shutdown by timeout from stored request" - def errors = bidResponse.ext?.errors - assert errors[ErrorType.GENERIC]*.code == [1] - assert errors[ErrorType.GENERIC]*.message == ["Timeout has been exceeded"] - } - - def "PBS auction should return error when auction.biddertmax.min value not enough for bidder request"() { - given: "PBS config with biddertmax.min" - def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.max" : MAX_TIMEOUT as String, - "auction.biddertmax.min" : MIN_TIMEOUT_BIDDER_REQUEST as String]) - - and: "Default BidRequest without timeout" - def bidRequest = BidRequest.defaultBidRequest.tap { - tmax = null - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) - } - - and: "Default stored request with min tmax" - def minTmax = MIN_TIMEOUT_BIDDER_REQUEST + 4 - def storedRequest = BidRequest.defaultStoredRequest.tap { - tmax = minTmax - } - - and: "Save storedRequest into DB" - def storedRequestModel = StoredRequest.getStoredRequest(bidRequest.ext.prebid.storedRequest.id, storedRequest) - storedRequestDao.save(storedRequestModel) - - when: "PBS processes auction request" - def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) - - then: "Bidder request timeout should correspond to the min from the stored request" - assert bidResponse?.ext?.debug?.resolvedRequest?.tmax == minTmax - - and: "PBS should send to bidder tmax form auction.biddertmax.min config" - assert bidResponse.ext.debug.httpcalls[BidderName.GENERIC.value]*.requestBody[0].contains("\"tmax\":${MIN_TIMEOUT_BIDDER_REQUEST}") - - and: "Bid response should shutdown by timeout from stored request" - def errors = bidResponse.ext?.errors - assert errors[ErrorType.GENERIC]*.code == [1] - assert errors[ErrorType.GENERIC]*.message == ["Timeout has been exceeded"] - } - def "PBS should choose min timeout form config for bidder request when in request value lowest that in auction.biddertmax.min"() { given: "PBS config with percent" def minBidderTmax = PBSUtils.getRandomNumber(MIN_TIMEOUT, MAX_TIMEOUT) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy index 09d8c51b0e1..041738cc5ab 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy @@ -93,7 +93,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert fetchBidsActivity.ruleConfiguration.every { it == null } assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) assert fetchBidsActivity.result.contains("DISALLOW") @@ -133,7 +133,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) assert fetchBidsActivity.ruleConfiguration.every { it == null } assert fetchBidsActivity.result.contains("ALLOW") @@ -148,7 +148,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitUfpdActivity.allowByDefault.contains(activity.defaultAction) assert transmitUfpdActivity.ruleConfiguration.every { it == null } assert transmitUfpdActivity.result.every { it == null } @@ -163,7 +163,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitPreciseGeoActivity.allowByDefault.contains(activity.defaultAction) assert transmitPreciseGeoActivity.ruleConfiguration.every { it == null } assert transmitPreciseGeoActivity.result.every { it == null } @@ -178,7 +178,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentName: GENERIC.value, componentType: BIDDER, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitTidActivity.allowByDefault.contains(activity.defaultAction) assert transmitTidActivity.ruleConfiguration.every { it == null } assert transmitTidActivity.result.every { it == null } @@ -227,7 +227,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentType: BIDDER, gpc: bidRequest.regs.ext.gpc, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert fetchBidsActivity.ruleConfiguration.contains(new RuleConfiguration( componentNames: condition.componentName, componentTypes: condition.componentType, @@ -265,7 +265,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { componentType: BIDDER, gpc: bidRequest.regs.ext.gpc, region: ALABAMA.abbreviation, - country: USA.value)) + country: USA.ISOAlpha3)) assert transmitPreciseGeoActivity.ruleConfiguration.every { it == null } assert transmitPreciseGeoActivity.allowByDefault.contains(activity.defaultAction) assert transmitPreciseGeoActivity.result.every { it == null } 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 d9df6ab076e..bb9d12de2fc 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 @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests.privacy +import org.mockserver.model.Delay import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountGdprConfig @@ -327,7 +328,7 @@ class GdprAmpSpec extends PrivacyBaseSpec { privacyPbsService.sendAmpRequest(ampRequest) then: "Used vendor list have proper specification version of GVL" - def properVendorListPath = "/app/prebid-server/data/vendorlist-v${tcfPolicyVersion.vendorListVersion}/${tcfPolicyVersion.vendorListVersion}.json" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion @@ -369,4 +370,64 @@ class GdprAmpSpec extends PrivacyBaseSpec { assert response.ext?.warnings[PREBID]*.message == ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] } + + def "PBS amp should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Prepare tcf consent string" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "AMP request" + def ampRequest = getGdprAmpRequest(tcfConsent) + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Reset valid vendor list response" + vendorListResponse.reset() + + and: "Set vendor list response with delay" + vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3)) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS shouldn't fetch vendor list" + def vendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + assert !privacyPbsService.isFileExist(vendorListPath) + + and: "Logs should contain proper vendor list version" + def logs = privacyPbsService.getLogsByTime(startTime) + def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading." + assert getLogsByText(logs, tcfError) + + and: "Second start for fetch second round of logs" + def secondStartTime = Instant.now() + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS shouldn't fetch vendor list" + assert !privacyPbsService.isFileExist(vendorListPath) + + and: "Logs should contain proper vendor list version" + def logsSecond = privacyPbsService.getLogsByTime(secondStartTime) + assert getLogsByText(logsSecond, tcfError) + + and: "Reset vendor list response" + vendorListResponse.reset() + + where: + tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V3] + } } 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 3d3ad19505c..d67b43e42c6 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 @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests.privacy +import org.mockserver.model.Delay import org.prebid.server.functional.model.ChannelType import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.request.auction.DistributionChannel @@ -275,7 +276,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { privacyPbsService.sendAuctionRequest(bidRequest) then: "Used vendor list have proper specification version of GVL" - def properVendorListPath = "/app/prebid-server/data/vendorlist-v${tcfPolicyVersion.vendorListVersion}/${tcfPolicyVersion.vendorListVersion}.json" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion @@ -313,4 +314,57 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] } + + def "PBS auction should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Reset valid vendor list response" + vendorListResponse.reset() + + and: "Set vendor list response with delay" + vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3)) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + assert !privacyPbsService.isFileExist(properVendorListPath) + + and: "Logs should contain proper vendor list version" + def logs = privacyPbsService.getLogsByTime(startTime) + def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading." + assert getLogsByText(logs, tcfError) + + and: "Second start for fetch second round of logs" + def secondStartTime = Instant.now() + + when: "PBS processes amp request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't fetch vendor list" + assert !privacyPbsService.isFileExist(properVendorListPath) + + and: "Logs should contain proper vendor list version" + def logsSecond = privacyPbsService.getLogsByTime(secondStartTime) + assert getLogsByText(logsSecond, tcfError) + + and: "Reset vendor list response" + vendorListResponse.reset() + + where: + tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V3] + } } 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 8ba421405f7..a2683a8b7ff 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 @@ -372,7 +372,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - null | [USA.value] + null | [USA.ISOAlpha3] new Geo(country: USA) | null new Geo(region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: CAN, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] @@ -419,7 +419,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } 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 4229a356033..42a096d2c55 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 @@ -1,12 +1,17 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.model.config.AccountCcpaConfig +import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountGppConfig +import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.config.AccountSetting import org.prebid.server.functional.model.config.ActivityConfig import org.prebid.server.functional.model.config.EqualityValueRule import org.prebid.server.functional.model.config.InequalityValueRule import org.prebid.server.functional.model.config.LogicalRestrictedRule import org.prebid.server.functional.model.config.GppModuleConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Activity import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities @@ -63,6 +68,7 @@ import static org.prebid.server.functional.model.request.auction.ActivityType.SY import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_ALL import static org.prebid.server.functional.model.request.auction.PrivacyModule.IAB_US_CUSTOM_LOGIC +import static org.prebid.server.functional.model.request.auction.PublicCountryIp.USA_IP import static org.prebid.server.functional.util.privacy.model.State.MANITOBA import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ALASKA @@ -75,13 +81,15 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { private static final String DISALLOWED_COUNT_FOR_ACTIVITY_RULE = "requests.activity.${SYNC_USER.metricValue}.disallowed.count" private static final String DISALLOWED_COUNT_FOR_GENERIC_ADAPTER = "adapter.${GENERIC.value}.activity.${SYNC_USER.metricValue}.disallowed.count" private static final String ALERT_GENERAL = "alerts.general" + private static final String GEO_LOCATION_REQUESTS = "geolocation_requests" + private static final String GEO_LOCATION_SUCCESSFUL = "geolocation_successful" private final static int INVALID_STATUS_CODE = 451 private final static String INVALID_STATUS_MESSAGE = "Unavailable For Legal Reasons." private static final Map GEO_LOCATION = ["geolocation.enabled" : "true", "geolocation.type" : "configuration", - "geolocation.configurations.[0].address-pattern": "209."] + "geolocation.configurations.[0].address-pattern": USA_IP.v4] def "PBS cookie sync call when bidder allowed in activities should include proper responded with bidders URLs and update processed metrics"() { given: "Cookie sync request with link to account" @@ -1683,12 +1691,12 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 where: - countyConfig | regionConfig | conditionGeo - null | null | ["$USA.value".toString()] - USA.value | ALABAMA.abbreviation | null - CAN.value | ALASKA.abbreviation | [USA.withState(ALABAMA)] - null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] - CAN.value | null | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + null | null | ["$USA.ISOAlpha3".toString()] + USA.ISOAlpha3 | ALABAMA.abbreviation | null + CAN.ISOAlpha3 | ALASKA.abbreviation | [USA.withState(ALABAMA)] + null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] + CAN.ISOAlpha3 | null | [USA.withState(ALABAMA)] } def "PBS setuid should process rule when geo doesn't intersection"() { @@ -1739,11 +1747,11 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 1 where: - countyConfig | regionConfig | conditionGeo - null | null | [USA.value] - CAN.value | ALASKA.abbreviation | [USA.withState(ALABAMA)] - null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] - CAN.value | null | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + null | null | [USA.ISOAlpha3] + CAN.ISOAlpha3 | ALASKA.abbreviation | [USA.withState(ALABAMA)] + null | MANITOBA.abbreviation | [USA.withState(ALABAMA)] + CAN.ISOAlpha3 | null | [USA.withState(ALABAMA)] } def "PBS cookie sync should disallowed rule when device.geo intersection"() { @@ -1792,9 +1800,9 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 where: - countyConfig | regionConfig | conditionGeo - USA.value | null | [USA.value] - USA.value | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + USA.ISOAlpha3 | null | [USA.ISOAlpha3] + USA.ISOAlpha3 | ALABAMA.abbreviation | [USA.withState(ALABAMA)] } def "PBS setuid should disallowed rule when device.geo intersection"() { @@ -1842,8 +1850,60 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE where: - countyConfig | regionConfig | conditionGeo - USA.value | null | [USA.value] - USA.value | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + countyConfig | regionConfig | conditionGeo + USA.ISOAlpha3 | null | [USA.ISOAlpha3] + USA.ISOAlpha3 | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + } + + def "PBS cookie sync should fetch geo once when gpp sync user and account require geo look up"() { + given: "Pbs config with geo location" + def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + + ["geolocation.configurations.[0].geo-info.country": USA.ISOAlpha3, + "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation]) + + and: "Cookie sync request with account connection" + def accountId = PBSUtils.randomNumber as String + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.account = accountId + it.gppSid = null + it.gdpr = null + } + + and: "Setup condition" + def condition = Condition.baseCondition.tap { + it.componentType = null + it.componentName = null + it.gppSid = null + it.geo = [USA.withState(ALABAMA)] + } + + and: "Set activity" + def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, activity) + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Set up account for allow activities" + def privacy = new AccountPrivacyConfig(ccpa: new AccountCcpaConfig(enabled: true), allowActivities: activities) + def accountConfig = new AccountConfig(privacy: privacy, settings: new AccountSetting(geoLookup: true)) + def account = new Account(uuid: accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes cookie sync request with header" + def response = prebidServerService + .sendCookieSyncRequest(cookieSyncRequest, ["X-Forwarded-For": USA_IP.v4]) + + then: "Response should not contain any URLs for bidders" + assert !response.bidderStatus.userSync.url + + and: "Metrics for disallowed activities should be updated" + def metrics = prebidServerService.sendCollectedMetricsRequest() + assert metrics[DISALLOWED_COUNT_FOR_ACTIVITY_RULE] == 1 + assert metrics[DISALLOWED_COUNT_FOR_GENERIC_ADAPTER] == 1 + + and: "Metrics processed across activities should be updated" + assert metrics[GEO_LOCATION_REQUESTS] == 1 + assert metrics[GEO_LOCATION_SUCCESSFUL] == 1 } } 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 ffecec814e2..a05f0e64b39 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 @@ -386,7 +386,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - null | [USA.value] + null | [USA.ISOAlpha3] new Geo(country: USA) | null new Geo(region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: CAN, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] @@ -438,7 +438,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } 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 431b66ab6cd..3372d8d0c15 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 @@ -471,7 +471,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.componentType = null it.componentName = [PBSUtils.randomString] it.gppSid = [USP_V1.intValue] - it.geo = ["$USA.value".toString()] + it.geo = ["$USA.ISOAlpha3".toString()] } and: "Set activity" @@ -561,7 +561,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } 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 5cd227ccef1..d056412e225 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 @@ -501,7 +501,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - null | [USA.value] + null | [USA.ISOAlpha3] new Geo(country: USA) | null new Geo(region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: CAN, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] @@ -564,7 +564,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { where: deviceGeo | conditionGeo - new Geo(country: USA) | [USA.value] + new Geo(country: USA) | [USA.ISOAlpha3] new Geo(country: USA, region: ALABAMA.abbreviation) | [USA.withState(ALABAMA)] new Geo(country: USA, region: ALABAMA.abbreviation) | [CAN.withState(ONTARIO), USA.withState(ALABAMA)] } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index 007c6767084..4a8a1d5e68e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -77,15 +77,27 @@ abstract class PrivacyBaseSpec extends BaseSpec { @Shared protected static final int PURPOSES_AND_LEG_INT_PURPOSES_GVL_VERSION = PBSUtils.getRandomNumberWithExclusion([PURPOSES_ONLY_GVL_VERSION, LEG_INT_PURPOSES_ONLY_GVL_VERSION, LEG_INT_AND_FLEXIBLE_PURPOSES_GVL_VERSION], 0, 4095) - protected static final Map PBS_CONFIG = OPENX_CONFIG + OPENX_COOKIE_SYNC_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG + protected static final int EXPONENTIAL_BACKOFF_MAX_DELAY = 1 + + private static final Map RETRY_POLICY_EXPONENTIAL_CONFIG = [ + "gdpr.vendorlist.v2.retry-policy.exponential-backoff.delay-millis" : 1 as String, + "gdpr.vendorlist.v2.retry-policy.exponential-backoff.max-delay-millis": EXPONENTIAL_BACKOFF_MAX_DELAY as String, + "gdpr.vendorlist.v2.retry-policy.exponential-backoff.factor" : Long.MAX_VALUE as String, + "gdpr.vendorlist.v3.retry-policy.exponential-backoff.delay-millis" : 1 as String, + "gdpr.vendorlist.v3.retry-policy.exponential-backoff.max-delay-millis": EXPONENTIAL_BACKOFF_MAX_DELAY as String, + "gdpr.vendorlist.v3.retry-policy.exponential-backoff.factor" : Long.MAX_VALUE as String] + + protected static final String VENDOR_LIST_PATH = "/app/prebid-server/data/vendorlist-v{VendorVersion}/{VendorVersion}.json" protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UspNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) @Shared protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG) + GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG) + + protected static final Map PBS_CONFIG = OPENX_CONFIG + OPENX_COOKIE_SYNC_CONFIG + + GENERIC_COOKIE_SYNC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG @Shared protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(PBS_CONFIG) diff --git a/src/test/groovy/org/prebid/server/functional/util/AllureReporter.groovy b/src/test/groovy/org/prebid/server/functional/util/AllureReporter.groovy deleted file mode 100644 index 6ada5fe95c9..00000000000 --- a/src/test/groovy/org/prebid/server/functional/util/AllureReporter.groovy +++ /dev/null @@ -1,314 +0,0 @@ -package org.prebid.server.functional.util - -import io.qameta.allure.Allure -import io.qameta.allure.AllureLifecycle -import io.qameta.allure.Description -import io.qameta.allure.Flaky -import io.qameta.allure.Muted -import io.qameta.allure.model.Label -import io.qameta.allure.model.Link -import io.qameta.allure.model.Parameter -import io.qameta.allure.model.Status -import io.qameta.allure.model.StatusDetails -import io.qameta.allure.model.TestResult -import io.qameta.allure.util.AnnotationUtils -import org.spockframework.runtime.AbstractRunListener -import org.spockframework.runtime.extension.IGlobalExtension -import org.spockframework.runtime.extension.builtin.UnrollIterationNameProvider -import org.spockframework.runtime.model.ErrorInfo -import org.spockframework.runtime.model.FeatureInfo -import org.spockframework.runtime.model.IterationInfo -import org.spockframework.runtime.model.MethodInfo -import org.spockframework.runtime.model.SpecInfo - -import java.lang.annotation.Annotation -import java.lang.annotation.Repeatable -import java.lang.reflect.Method -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.util.stream.Collectors -import java.util.stream.Stream - -import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel -import static io.qameta.allure.util.ResultsUtils.createHostLabel -import static io.qameta.allure.util.ResultsUtils.createLanguageLabel -import static io.qameta.allure.util.ResultsUtils.createPackageLabel -import static io.qameta.allure.util.ResultsUtils.createParameter -import static io.qameta.allure.util.ResultsUtils.createParentSuiteLabel -import static io.qameta.allure.util.ResultsUtils.createSubSuiteLabel -import static io.qameta.allure.util.ResultsUtils.createSuiteLabel -import static io.qameta.allure.util.ResultsUtils.createTestClassLabel -import static io.qameta.allure.util.ResultsUtils.createTestMethodLabel -import static io.qameta.allure.util.ResultsUtils.createThreadLabel -import static io.qameta.allure.util.ResultsUtils.firstNonEmpty -import static io.qameta.allure.util.ResultsUtils.getProvidedLabels -import static io.qameta.allure.util.ResultsUtils.getStatus -import static io.qameta.allure.util.ResultsUtils.getStatusDetails -import static java.nio.charset.StandardCharsets.UTF_8 -import static java.util.Comparator.comparing -import static org.apache.commons.lang3.StringUtils.EMPTY - -/** - * This is a temporary port of https://github.com/allure-framework/allure-java/tree/master/allure-spock to add support - * for Spock 2.0. - * **/ -class AllureReporter extends AbstractRunListener implements IGlobalExtension { - - private static final String FRAMEWORK = "spock" - private static final String LANGUAGE = "groovy" - private static final String MD5 = "md5" - private static final String GIVEN = "Given:" - private static final String WHEN = "When:" - private static final String THEN = "Then:" - private static final String EXPECT = "Expect:" - private static final String WHERE = "Where:" - private static final String AND = "And:" - private static final String CLEANUP = "Cleanup:" - - private final Map stepSpockMap = new HashMap<>() - private final ThreadLocal testUuid - = InheritableThreadLocal.withInitial({ UUID.randomUUID().toString() }) - - private final AllureLifecycle lifecycle - - AllureReporter() { - this(Allure.getLifecycle()) - - this.stepSpockMap.put("SETUP", GIVEN) - this.stepSpockMap.put("WHEN", WHEN) - this.stepSpockMap.put("THEN", THEN) - this.stepSpockMap.put("EXPECT", EXPECT) - this.stepSpockMap.put("WHERE", WHERE) - this.stepSpockMap.put("AND", AND) - this.stepSpockMap.put("CLEANUP", CLEANUP) - } - - AllureReporter(AllureLifecycle lifecycle) { - this.lifecycle = lifecycle - } - - @Override - void visitSpec(SpecInfo spec) { - spec.addListener(this) - } - - @Override - void beforeIteration(IterationInfo iteration) { - String uuid = testUuid.get() - FeatureInfo feature = iteration.feature - SpecInfo spec = feature.spec - List parameters = getParameters(iteration.dataVariables) - SpecInfo subSpec = spec.subSpec - SpecInfo superSpec = spec.superSpec - String packageName = spec.package - String specName = spec.name - String testClassName = spec.reflection.name - String testMethodName = iteration.displayName - - List"); } + @Test + public void createBidVastXmlShouldBeModifiedWithNewImpressionVastUrlWhenEventsEnabledAndNoEmptyTag2() { + // when + final String bidAdm = "< impreSSion garbage >http:/test.com< /ImPression garbage >"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo("< impreSSion garbage >http:/test.com< /ImPression garbage >" + + ""); + } + @Test public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpressionTags() { // when @@ -192,6 +206,22 @@ public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpres + ""); } + @Test + public void createBidVastXmlShouldBeModifiedWithNewImpressionAfterExistingImpressionTags2() { + // when + final String bidAdm = "< Impression >http:/test.com< /Impression >" + + "http:/test2.com< /ImPRession garbage>"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo("< Impression >http:/test.com< /Impression >" + + "http:/test2.com< /ImPRession garbage>" + + ""); + } + @Test public void createBidVastXmlShouldInsertImpressionTagForEmptyInLine() { // when @@ -232,11 +262,26 @@ public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode() { + ""); } + @Test + public void createBidVastXmlShouldModifyWrapperTagInCaseInsensitiveMode2() { + // when + final String bidAdm = "< wraPPer garbage>http:/test.com< / wraPPer garbage>"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo("< wraPPer garbage>http:/test.com" + + "< / wraPPer garbage>"); + } + @Test public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper() { // when + final String bidAdm = ""; final String result = target.createBidVastXml( - BIDDER, "", BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); // then verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); @@ -245,6 +290,22 @@ public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper() { .isEqualTo(""); } + @Test + public void createBidVastXmlShouldInsertImpressionTagForEmptyWrapper2() { + // when + final String bidAdm = "< wraPPer garbage>< / wrapPer garbage>"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, + BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result) + .isEqualTo("< wraPPer garbage>< / wrapPer garbage>"); + } + @Test public void createBidVastXmlShouldNotInsertImpressionTagForNoWrapperCloseTag() { // when @@ -272,6 +333,20 @@ public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode() { + ""); } + @Test + public void createBidVastXmlShouldModifyInlineTagInCaseInsensitiveMode2() { + // when + final String bidAdm = "< InLIne garbage >http:/test.com"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo("< InLIne garbage >http:/test.com" + + ""); + } + @Test public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags() { // when @@ -285,6 +360,20 @@ public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags() { assertThat(result).isEqualTo(""); } + @Test + public void createBidVastXmlShouldBeModifiedIfInLineHasNoImpressionTags2() { + // when + final String bidAdm = "< InLIne garbage >< / InLIne garbage >"; + final String result = target + .createBidVastXml(BIDDER, bidAdm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), emptyList()); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + + assertThat(result).isEqualTo("< InLIne garbage >< / InLIne garbage >"); + } + @Test public void createBidVastXmlShouldNotBeModifiedIfNoParentTagsPresent() { // when @@ -300,6 +389,36 @@ public void createBidVastXmlShouldNotBeModifiedIfNoParentTagsPresent() { verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse); } + @Test + public void createBidVastXmlShouldNotBeModifiedIfWrapperTagIsInvalid() { + // when + final String adm = ""; + final List warnings = new ArrayList<>(); + final String result = target + .createBidVastXml(BIDDER, adm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), warnings); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + assertThat(result).isEqualTo(adm); + assertThat(warnings).containsExactly("VastXml does not contain neither InLine nor Wrapper for bidder response"); + verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse); + } + + @Test + public void createBidVastXmlShouldNotBeModifiedIfInlineTagIsInvalid() { + // when + final String adm = ""; + final List warnings = new ArrayList<>(); + final String result = target + .createBidVastXml(BIDDER, adm, BID_NURL, BID_ID, ACCOUNT_ID, eventsContext(), warnings); + + // then + verify(eventsService).vastUrlTracking(BID_ID, BIDDER, ACCOUNT_ID, eventsContext()); + assertThat(result).isEqualTo(adm); + assertThat(warnings).containsExactly("VastXml does not contain neither InLine nor Wrapper for bidder response"); + verify(metrics).updateAdapterRequestErrorMetric(BIDDER, MetricName.badserverresponse); + } + @Test public void createBidVastXmlShouldNotModifyWhenEventsEnabledAndAdmHaveNoImpression() { // when diff --git a/src/test/resources/META-INF/aop-ajc.xml b/src/test/resources/META-INF/aop-ajc.xml deleted file mode 100644 index 09e38300cea..00000000000 --- a/src/test/resources/META-INF/aop-ajc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension index f62223f5bfe..e86265025dd 100644 --- a/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension +++ b/src/test/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension @@ -1,2 +1 @@ org.prebid.server.functional.testcontainers.TestcontainersExtension -org.prebid.server.functional.util.AllureReporter diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json index 8b5fa586f61..c0707bf2a61 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json @@ -14,5 +14,8 @@ "metaData": { "usi": "user_d" }, - "context": "some_page" + "context": "some_page", + "kv" : { + "any" : "any" + } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-request.json index 7bec0900f3e..5391bd8bd0f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-request.json @@ -1,10 +1,27 @@ { "id": "request_id", "user": { - "id": "user_d" + "id": "user_d", + "ext": { + "eids": [ + { + "source": "adserver.org", + "uids": [ + { + "id": "id" + } + ] + } + ] + } }, "site": { - "page": "some_page" + "page": "some_page", + "ext": { + "data": { + "any": "any" + } + } }, "imp": [ { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json index 35a9d7ca47f..0c39fe64552 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-adprime-bid-response.json @@ -10,10 +10,11 @@ "crid": "crid001", "adm": "adm001", "h": 250, - "w": 300 + "w": 300, + "mtype": 1 } ] } ], "bidid": "bid_id" -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json index 157e64739a3..5b7e94562fd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json @@ -11,6 +11,7 @@ "crid": "crid001", "w": 300, "h": 250, + "mtype": 1, "ext": { "prebid": { "type": "banner" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json index 2fae1241d83..bfbeccf737f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json @@ -9,6 +9,7 @@ }, "ext": { "bizzclick": { + "host": "host", "accountId": "accountId", "placementId": "placementId" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json index ec5eadf42d0..4a6e48aac57 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json @@ -8,6 +8,7 @@ "impid": "imp_id", "price": 3.33, "adm": "adm001", + "mtype": 1, "adid": "adid001", "cid": "cid001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-iqzone-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-iqzone-bid-response.json index 31a6f4419e3..26af1edc5a3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-iqzone-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-iqzone-bid-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "impid": "imp_id", "price": 3.33, + "mtype": 1, "adid": "adid001", "crid": "crid001", "cid": "cid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-request.json new file mode 100644 index 00000000000..77e4bcbe174 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "smrtconnect": { + "supply_id": "1" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json new file mode 100644 index 00000000000..90d63b63f15 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.5 + } + } + ], + "seat": "smrtconnect", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": "{{ smrtconnect.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-smrtconnect-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-smrtconnect-bid-request.json new file mode 100644 index 00000000000..9bec1f0c932 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-smrtconnect-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "supply_id": "1" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-smrtconnect-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-smrtconnect-bid-response.json new file mode 100644 index 00000000000..b68c3c92bba --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-smrtconnect-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "seat": "smrtconnect", + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "bidid": "bid_id" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-request.json new file mode 100644 index 00000000000..afa10f0938d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "zmaticoo": { + "pubId": "pubTestID", + "zoneId": "zoneTestID" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json new file mode 100644 index 00000000000..f2bb11ef448 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "mtype": 1, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "zmaticoo", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "zmaticoo": "{{ zmaticoo.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-zmaticoo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-zmaticoo-bid-request.json new file mode 100644 index 00000000000..46e80d08b96 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-zmaticoo-bid-request.json @@ -0,0 +1,57 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "pubId": "pubTestID", + "zoneId": "zoneTestID" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-zmaticoo-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-zmaticoo-bid-response.json new file mode 100644 index 00000000000..891438f2401 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-zmaticoo-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "mtype": 1, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} 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 fc44352e7c3..29114f9d3fb 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -112,7 +112,7 @@ adapters.bidscube.endpoint=http://localhost:8090/bidscube-exchange adapters.bidstack.enabled=true adapters.bidstack.endpoint=http://localhost:8090/bidstack-exchange adapters.bizzclick.enabled=true -adapters.bizzclick.endpoint=http://localhost:8090/bizzclick-exchange +adapters.bizzclick.endpoint=http://localhost:8090/bizzclick-exchange?host={{Host}}&source={{SourceId}}&account={{AccountID}} adapters.bliink.enabled=true adapters.bliink.endpoint=http://localhost:8090/bliink-exchange adapters.bluesea.enabled=true @@ -351,6 +351,8 @@ adapters.smartyads.enabled=true adapters.smartyads.endpoint=http://localhost:8090/smartyads-exchange adapters.smilewanted.enabled=true adapters.smilewanted.endpoint=http://localhost:8090/smilewanted-exchange +adapters.smrtconnect.enabled=true +adapters.smrtconnect.endpoint=http://localhost:8090/smrtconnect-exchange?supply_id={{SupplyId}} adapters.sonobi.enabled=true adapters.sonobi.endpoint=http://localhost:8090/sonobi-exchange adapters.sovrn.enabled=true @@ -445,6 +447,8 @@ adapters.aax.enabled=true adapters.aax.endpoint=http://localhost:8090/aax-exchange adapters.zeta_global_ssp.enabled=true adapters.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange +adapters.zmaticoo.enabled=true +adapters.zmaticoo.endpoint=http://localhost:8090/zmaticoo-exchange adapters.yearxero.enabled=true adapters.yearxero.endpoint=http://localhost:8090/yearxero-exchange adapters.minutemedia.enabled=true