diff --git a/pom.xml b/pom.xml index 77687dbaf52..73e7a7e8bc6 100644 --- a/pom.xml +++ b/pom.xml @@ -60,14 +60,14 @@ 4.0.0 3.21.0 2.31.0 - 4.1.0 + 4.1.1 9.4.44.v20210927 4.4.0 2.1.210 2.0-groovy-3.0 - 1.16.2 + 1.16.3 5.11.2 - 2.16.1 + 2.17.2 1.9.7 1.4.200 1.11.19 diff --git a/src/main/java/org/prebid/server/analytics/pubstack/model/EventType.java b/src/main/java/org/prebid/server/analytics/pubstack/model/EventType.java deleted file mode 100644 index f4b1cbe0460..00000000000 --- a/src/main/java/org/prebid/server/analytics/pubstack/model/EventType.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.prebid.server.analytics.pubstack.model; - -public enum EventType { - - auction, cookiesync, amp, setuid, video -} - diff --git a/src/main/java/org/prebid/server/analytics/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java similarity index 97% rename from src/main/java/org/prebid/server/analytics/AnalyticsReporterDelegator.java rename to src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java index 4da8f31d769..052296d9ce4 100644 --- a/src/main/java/org/prebid/server/analytics/AnalyticsReporterDelegator.java +++ b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics; +package org.prebid.server.analytics.reporter; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -11,6 +11,7 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.model.CookieSyncEvent; @@ -59,6 +60,7 @@ public AnalyticsReporterDelegator(List delegates, Vertx vertx, PrivacyEnforcementService privacyEnforcementService, Metrics metrics) { + this.delegates = Objects.requireNonNull(delegates); this.vertx = Objects.requireNonNull(vertx); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); @@ -82,6 +84,7 @@ public void processEvent(T event, TcfContext tcfContext) { private void delegateEvent(T event, TcfContext tcfContext, AsyncResult> privacyEnforcementMapResult) { + if (privacyEnforcementMapResult.succeeded()) { final Map privacyEnforcementActionMap = privacyEnforcementMapResult.result(); @@ -150,6 +153,7 @@ private static T updateEvent(T event, String adapter) { return event; } + @SuppressWarnings("ConstantConditions") private static AuctionContext updateAuctionContextAdapter(AuctionContext context, String adapter) { final BidRequest bidRequest = context != null ? context.getBidRequest() : null; final BidRequest updatedBidRequest = updateBidRequest(bidRequest, adapter); @@ -215,21 +219,23 @@ private Future processFail(Throwable exception, T event, String report private void updateMetricsByEventType(T event, String analyticsCode, MetricName result) { final MetricName eventType; - if (event instanceof AuctionEvent) { - eventType = MetricName.event_auction; - } else if (event instanceof AmpEvent) { + + if (event instanceof AmpEvent) { eventType = MetricName.event_amp; - } else if (event instanceof VideoEvent) { - eventType = MetricName.event_video; - } else if (event instanceof SetuidEvent) { - eventType = MetricName.event_setuid; + } else if (event instanceof AuctionEvent) { + eventType = MetricName.event_auction; } else if (event instanceof CookieSyncEvent) { eventType = MetricName.event_cookie_sync; } else if (event instanceof NotificationEvent) { eventType = MetricName.event_notification; + } else if (event instanceof SetuidEvent) { + eventType = MetricName.event_setuid; + } else if (event instanceof VideoEvent) { + eventType = MetricName.event_video; } else { eventType = MetricName.event_unknown; } + metrics.updateAnalyticEventMetric(analyticsCode, eventType, result); } } diff --git a/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java similarity index 61% rename from src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java rename to src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java index 14e472c1226..309b75e9d7b 100644 --- a/src/main/java/org/prebid/server/analytics/LogAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporter.java @@ -1,16 +1,16 @@ -package org.prebid.server.analytics; +package org.prebid.server.analytics.reporter.log; -import com.fasterxml.jackson.annotation.JsonUnwrapped; import io.vertx.core.Future; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; -import lombok.AllArgsConstructor; -import lombok.Value; +import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.model.CookieSyncEvent; +import org.prebid.server.analytics.model.NotificationEvent; import org.prebid.server.analytics.model.SetuidEvent; import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.log.model.LogEvent; import org.prebid.server.json.JacksonMapper; import java.util.Objects; @@ -32,21 +32,24 @@ public LogAnalyticsReporter(JacksonMapper mapper) { public Future processEvent(T event) { final LogEvent logEvent; - if (event instanceof AuctionEvent) { - logEvent = new LogEvent<>("/openrtb2/auction", ((AuctionEvent) event).getBidResponse()); - } else if (event instanceof AmpEvent) { - logEvent = new LogEvent<>("/openrtb2/amp", ((AmpEvent) event).getBidResponse()); - } else if (event instanceof VideoEvent) { - logEvent = new LogEvent<>("/openrtb2/video", ((VideoEvent) event).getBidResponse()); + if (event instanceof AmpEvent) { + logEvent = LogEvent.of("/openrtb2/amp", ((AmpEvent) event).getBidResponse()); + } else if (event instanceof AuctionEvent) { + logEvent = LogEvent.of("/openrtb2/auction", ((AuctionEvent) event).getBidResponse()); + } else if (event instanceof CookieSyncEvent) { + logEvent = LogEvent.of("/cookie_sync", ((CookieSyncEvent) event).getBidderStatus()); + } else if (event instanceof NotificationEvent) { + final NotificationEvent notificationEvent = (NotificationEvent) event; + logEvent = LogEvent.of("/event", notificationEvent.getType() + notificationEvent.getBidId()); } else if (event instanceof SetuidEvent) { final SetuidEvent setuidEvent = (SetuidEvent) event; - logEvent = new LogEvent<>( + logEvent = LogEvent.of( "/setuid", setuidEvent.getBidder() + ":" + setuidEvent.getUid() + ":" + setuidEvent.getSuccess()); - } else if (event instanceof CookieSyncEvent) { - logEvent = new LogEvent<>("/cookie_sync", ((CookieSyncEvent) event).getBidderStatus()); + } else if (event instanceof VideoEvent) { + logEvent = LogEvent.of("/openrtb2/video", ((VideoEvent) event).getBidResponse()); } else { - logEvent = new LogEvent<>("unknown", null); + logEvent = LogEvent.of("unknown", null); } logger.debug(mapper.encodeToString(logEvent)); @@ -63,14 +66,4 @@ public int vendorId() { public String name() { return "logAnalytics"; } - - @AllArgsConstructor - @Value - private static class LogEvent { - - String type; - - @JsonUnwrapped - T event; - } } diff --git a/src/main/java/org/prebid/server/analytics/reporter/log/model/LogEvent.java b/src/main/java/org/prebid/server/analytics/reporter/log/model/LogEvent.java new file mode 100644 index 00000000000..35aae10ecef --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/log/model/LogEvent.java @@ -0,0 +1,13 @@ +package org.prebid.server.analytics.reporter.log.model; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import lombok.Value; + +@Value(staticConstructor = "of") +public class LogEvent { + + String type; + + @JsonUnwrapped + T event; +} diff --git a/src/main/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java similarity index 87% rename from src/main/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporter.java rename to src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java index 2f068951c7a..496d9f56ab6 100644 --- a/src/main/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporter.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics.pubstack; +package org.prebid.server.analytics.reporter.pubstack; import io.vertx.core.AsyncResult; import io.vertx.core.Future; @@ -11,11 +11,12 @@ import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.model.CookieSyncEvent; +import org.prebid.server.analytics.model.NotificationEvent; import org.prebid.server.analytics.model.SetuidEvent; import org.prebid.server.analytics.model.VideoEvent; -import org.prebid.server.analytics.pubstack.model.EventType; -import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties; -import org.prebid.server.analytics.pubstack.model.PubstackConfig; +import org.prebid.server.analytics.reporter.pubstack.model.EventType; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackConfig; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -26,7 +27,6 @@ import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Function; @@ -38,16 +38,6 @@ public class PubstackAnalyticsReporter implements AnalyticsReporter, Initializab private static final String EVENT_REPORT_ENDPOINT_PATH = "/intake"; private static final String CONFIG_URL_SUFFIX = "/bootstrap?scopeId="; - private static final Map CLASS_TO_EVENT_TYPE; - - static { - CLASS_TO_EVENT_TYPE = new HashMap<>(); - CLASS_TO_EVENT_TYPE.put(AuctionEvent.class.getName(), EventType.auction); - CLASS_TO_EVENT_TYPE.put(AmpEvent.class.getName(), EventType.amp); - CLASS_TO_EVENT_TYPE.put(VideoEvent.class.getName(), EventType.video); - CLASS_TO_EVENT_TYPE.put(SetuidEvent.class.getName(), EventType.setuid); - CLASS_TO_EVENT_TYPE.put(CookieSyncEvent.class.getName(), EventType.cookiesync); - } private final long configurationRefreshDelay; private final long timeout; @@ -62,6 +52,7 @@ public PubstackAnalyticsReporter(PubstackAnalyticsProperties pubstackAnalyticsPr HttpClient httpClient, JacksonMapper jacksonMapper, Vertx vertx) { + this.configurationRefreshDelay = Objects.requireNonNull(pubstackAnalyticsProperties.getConfigurationRefreshDelayMs()); this.timeout = Objects.requireNonNull(pubstackAnalyticsProperties.getTimeoutMs()); @@ -79,6 +70,7 @@ private static Map createEventHandlers( HttpClient httpClient, JacksonMapper jacksonMapper, Vertx vertx) { + return Arrays.stream(EventType.values()) .collect(Collectors.toMap(Function.identity(), eventType -> new PubstackEventHandler( @@ -94,11 +86,30 @@ private static String buildEventEndpointUrl(String endpoint, EventType eventType return HttpUtil.validateUrl(endpoint + EVENT_REPORT_ENDPOINT_PATH + eventType.name()); } + @Override public Future processEvent(T event) { - final EventType eventType = CLASS_TO_EVENT_TYPE.get(event.getClass().getName()); + final EventType eventType; + + if (event instanceof AmpEvent) { + eventType = EventType.amp; + } else if (event instanceof AuctionEvent) { + eventType = EventType.auction; + } else if (event instanceof CookieSyncEvent) { + eventType = EventType.cookiesync; + } else if (event instanceof NotificationEvent) { + eventType = EventType.notification; + } else if (event instanceof SetuidEvent) { + eventType = EventType.setuid; + } else if (event instanceof VideoEvent) { + eventType = EventType.video; + } else { + eventType = null; + } + if (eventType != null) { eventHandlers.get(eventType).handle(event); } + return Future.succeededFuture(); } diff --git a/src/main/java/org/prebid/server/analytics/pubstack/PubstackEventHandler.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java similarity index 97% rename from src/main/java/org/prebid/server/analytics/pubstack/PubstackEventHandler.java rename to src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java index 20f8ee8dff6..5df6fcec5c7 100644 --- a/src/main/java/org/prebid/server/analytics/pubstack/PubstackEventHandler.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandler.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics.pubstack; +package org.prebid.server.analytics.reporter.pubstack; import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.handler.codec.http.HttpResponseStatus; @@ -9,7 +9,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; -import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.util.HttpUtil; @@ -148,8 +148,8 @@ private static byte[] toGzippedBytes(Queue events) { } private static byte[] gzip(String value) { - try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream( - obj)) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { gzip.write(value.getBytes(StandardCharsets.UTF_8)); gzip.finish(); diff --git a/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/EventType.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/EventType.java new file mode 100644 index 00000000000..24fde84cc3f --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/EventType.java @@ -0,0 +1,7 @@ +package org.prebid.server.analytics.reporter.pubstack.model; + +public enum EventType { + + amp, auction, cookiesync, notification, setuid, video +} + diff --git a/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackAnalyticsProperties.java similarity index 83% rename from src/main/java/org/prebid/server/analytics/pubstack/model/PubstackAnalyticsProperties.java rename to src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackAnalyticsProperties.java index 8987202c56e..9de49532984 100644 --- a/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackAnalyticsProperties.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackAnalyticsProperties.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics.pubstack.model; +package org.prebid.server.analytics.reporter.pubstack.model; import lombok.Builder; import lombok.Value; diff --git a/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackConfig.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java similarity index 84% rename from src/main/java/org/prebid/server/analytics/pubstack/model/PubstackConfig.java rename to src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java index 9815fbf5047..27562b9e40e 100644 --- a/src/main/java/org/prebid/server/analytics/pubstack/model/PubstackConfig.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics.pubstack.model; +package org.prebid.server.analytics.reporter.pubstack.model; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 33ecec313ad..0baf340de5c 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -127,6 +127,7 @@ public class RubiconBidder implements Bidder { private static final String ADSERVER_EID = "adserver.org"; private static final String LIVEINTENT_EID = "liveintent.com"; private static final String LIVERAMP_EID = "liveramp.com"; + private static final String SOURCE_RUBICON = "rubiconproject.com"; private static final String FPD_GPID_FIELD = "gpid"; private static final String FPD_SECTIONCAT_FIELD = "sectioncat"; @@ -144,6 +145,7 @@ public class RubiconBidder implements Bidder { private static final String PREBID_EXT = "prebid"; private static final String PPUID_STYPE = "ppuid"; + private static final String OTHER_STYPE = "other"; private static final String SHA256EMAIL_STYPE = "sha256email"; private static final String DMP_STYPE = "dmp"; private static final String XAPI_CURRENCY = "USD"; @@ -179,7 +181,7 @@ public RubiconBidder(String endpoint, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); - this.supportedVendors = new HashSet<>(supportedVendors); + this.supportedVendors = Set.copyOf(Objects.requireNonNull(supportedVendors)); this.generateBidId = generateBidId; this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); @@ -194,12 +196,11 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List imps = extractValidImps(bidRequest, errors); if (CollectionUtils.isEmpty(imps)) { - errors.add(BidderError.of("There are no valid impressions to create bid request to rubicon bidder", - BidderError.Type.bad_input)); - return Result.of(Collections.emptyList(), errors); + errors.add(BidderError.badInput("There are no valid impressions to create bid request to rubicon bidder")); + return Result.withErrors(errors); } - final Map> impToImpExt = - parseRubiconImpExts(imps, errors); + + final Map> impToImpExt = parseRubiconImpExts(imps, errors); final String impLanguage = firstImpExtLanguage(impToImpExt.values()); final String uri = makeUri(bidRequest); @@ -270,9 +271,8 @@ private static boolean isValidType(Imp imp) { private BidderError impTypeErrorMessage(Imp imp) { final BidType type = resolveExpectedBidType(imp); - return BidderError.of( - String.format("Impression with id %s rejected with invalid type `%s`." + " Allowed types are banner and" - + " video.", imp.getId(), type != null ? type.name() : "unknown"), BidderError.Type.bad_input); + return BidderError.badInput(String.format("Impression with id %s rejected with invalid type `%s`." + + " Allowed types are banner and video.", imp.getId(), type != null ? type.name() : "unknown")); } private static BidType resolveExpectedBidType(Imp imp) { @@ -938,6 +938,8 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { final ExtUser extUser = user != null ? user.getExt() : null; final String resolvedId = userId == null ? resolveUserId(extUser) : null; final List extUserEids = extUser != null ? extUser.getEids() : null; + final String userBuyeruid = user != null ? user.getBuyeruid() : null; + final String resolvedBuyeruid = userBuyeruid != null ? userBuyeruid : resolveBuyeruidFromEids(extUserEids); final Map> sourceToUserEidExt = extUser != null ? specialExtUserEids(extUserEids) : null; @@ -952,8 +954,13 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { final ObjectNode userExtData = extUser != null ? extUser.getData() : null; final String liverampId = extractLiverampId(sourceToUserEidExt); - if (userExtRp == null && userExtTpIds == null && userExtData == null && liverampId == null - && resolvedId == null && !hasStypeToRemove) { + if (userExtRp == null + && userExtTpIds == null + && userExtData == null + && liverampId == null + && resolvedId == null + && Objects.equals(userBuyeruid, resolvedBuyeruid) + && !hasStypeToRemove) { return user; } @@ -975,6 +982,7 @@ private User makeUser(User user, ExtImpRubicon rubiconImpExt) { return userBuilder .id(ObjectUtils.defaultIfNull(resolvedId, userId)) + .buyeruid(resolvedBuyeruid) .gender(null) .yob(null) .geo(null) @@ -1041,6 +1049,28 @@ private static ExtUserEidUid cleanExtUserEidUidStype(ExtUserEidUid extUserEidUid ExtUserEidUidExt.of(extUserEidUidExt.getRtiPartner(), null)); } + private static String resolveBuyeruidFromEids(List eids) { + return CollectionUtils.emptyIfNull(eids).stream() + .filter(Objects::nonNull) + .filter(eid -> SOURCE_RUBICON.equals(eid.getSource())) + .map(ExtUserEid::getUids) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .filter(RubiconBidder::validateExtUserEidUidForUserBuyeruid) + .map(ExtUserEidUid::getId) + .findFirst() + .orElse(null); + + } + + private static boolean validateExtUserEidUidForUserBuyeruid(ExtUserEidUid uid) { + final ExtUserEidUidExt uidExt = ObjectUtil.getIfNotNull(uid, ExtUserEidUid::getExt); + final String uidExtStype = ObjectUtil.getIfNotNull(uidExt, ExtUserEidUidExt::getStype); + + return StringUtils.equalsAny(uidExtStype, PPUID_STYPE, OTHER_STYPE); + } + private static Map> specialExtUserEids(List eids) { if (CollectionUtils.isEmpty(eids)) { return null; diff --git a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java index 4b7cc3078bf..5cbafc51ae8 100644 --- a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java +++ b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java @@ -7,7 +7,7 @@ import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -27,141 +27,142 @@ import java.math.BigDecimal; import java.net.URISyntaxException; +import java.time.Clock; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class TappxBidder implements Bidder { - private static final String VERSION = "1.3"; + private static final String VERSION = "1.4"; private static final String TYPE_CNN = "prebid"; private static final TypeReference> TAPX_EXT_TYPE_REFERENCE = new TypeReference<>() { }; + private static final Pattern NEW_ENDPOINT_PATTERN = Pattern.compile("^(zz|vz)[0-9]{3,}([a-z]{2}|test)$"); + private static final String SUBDOMAIN_MACRO = "{{subdomain}}"; private final String endpointUrl; + private final Clock clock; private final JacksonMapper mapper; - public TappxBidder(String endpointUrl, JacksonMapper mapper) { + public TappxBidder(String endpointUrl, Clock clock, JacksonMapper mapper) { this.endpointUrl = Objects.requireNonNull(endpointUrl); + this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { + final List imps = request.getImp(); + final ExtImpTappx extImpTappx; final String url; try { - extImpTappx = parseBidRequestToExtImpTappx(request); + extImpTappx = parseImpExt(imps.get(0)); url = resolveUrl(extImpTappx, request.getTest()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } - final BidRequest outgoingRequest = modifyRequest(request, extImpTappx); - return Result.withValue(HttpRequest.builder() - .method(HttpMethod.POST) - .headers(HttpUtil.headers()) - .uri(url) - .body(mapper.encodeToBytes(outgoingRequest)) - .payload(outgoingRequest) - .build()); + final BidRequest modifiedRequest = modifyRequest(request, modifyImps(imps, extImpTappx), extImpTappx); + return Result.withValue(makeHttpRequest(modifiedRequest, url)); } - /** - * Retrieves first {@link ExtImpTappx} from {@link Imp}. - */ - private ExtImpTappx parseBidRequestToExtImpTappx(BidRequest request) { + private ExtImpTappx parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(request.getImp().get(0).getExt(), TAPX_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), TAPX_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage(), e); } } - /** - * Builds endpoint url based on adapter-specific pub settings from imp.ext. - */ + private static List modifyImps(List imps, ExtImpTappx extImpTappx) { + List modifiedImps = new ArrayList<>(imps); + modifiedImps.set(0, modifyImp(imps.get(0), extImpTappx)); + + return modifiedImps; + } + private String resolveUrl(ExtImpTappx extImpTappx, Integer test) { - final String host = extImpTappx.getHost(); - if (StringUtils.isBlank(host)) { - throw new PreBidException("Tappx host undefined"); - } + final String subdomain = extImpTappx.getEndpoint(); + final boolean isNewEndpoint = NEW_ENDPOINT_PATTERN.matcher(subdomain).matches(); - final String endpoint = extImpTappx.getEndpoint(); - if (StringUtils.isBlank(endpoint)) { - throw new PreBidException("Tappx endpoint undefined"); + final String baseUri = isNewEndpoint ? resolveNewHost(subdomain) : resolveOldHost(); + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(baseUri); + } catch (URISyntaxException e) { + throw new PreBidException(String.format("Failed to build endpoint URL: %s", e.getMessage())); } - final String tappxkey = extImpTappx.getTappxkey(); - if (StringUtils.isBlank(tappxkey)) { - throw new PreBidException("Tappx tappxkey undefined"); + if (!isNewEndpoint) { + final List pathSegments = uriBuilder.getPathSegments(); + uriBuilder.setPathSegments(ListUtils.union(pathSegments, Collections.singletonList(subdomain))); } - return buildUrl(host, endpoint, tappxkey, test); - } + uriBuilder.addParameter("tappxkey", extImpTappx.getTappxkey()); + uriBuilder.addParameter("v", VERSION); + uriBuilder.addParameter("type_cnn", TYPE_CNN); - private String buildUrl(String host, String endpoint, String tappxkey, Integer test) { - try { - final String baseUri = resolveBaseUri(host); - final URIBuilder uriBuilder = new URIBuilder(baseUri); - - if (!StringUtils.containsIgnoreCase(host, endpoint)) { - final List pathSegments = new ArrayList<>(); - uriBuilder.getPathSegments().stream() - .filter(StringUtils::isNotBlank) - .forEach(pathSegments::add); - pathSegments.add(StringUtils.strip(endpoint, "/")); - uriBuilder.setPathSegments(pathSegments); - } + if (test != null && test == 0) { + uriBuilder.addParameter("ts", String.valueOf(clock.millis())); + } - uriBuilder.addParameter("tappxkey", tappxkey); - uriBuilder.addParameter("v", VERSION); - uriBuilder.addParameter("type_cnn", TYPE_CNN); + return uriBuilder.toString(); + } - if (test != null && test == 0) { - final String ts = String.valueOf(System.nanoTime()); - uriBuilder.addParameter("ts", ts); - } - return uriBuilder.build().toString(); - } catch (URISyntaxException e) { - throw new PreBidException(String.format("Failed to build endpoint URL: %s", e.getMessage())); - } + private String resolveNewHost(String subdomain) { + return endpointUrl.replace(SUBDOMAIN_MACRO, subdomain + ".pub") + "/rtb/"; } - private String resolveBaseUri(String host) { - return StringUtils.startsWithAny(host.toLowerCase(), "http://", "https://") - ? host - : endpointUrl + host; + private String resolveOldHost() { + return endpointUrl.replace(SUBDOMAIN_MACRO, "ssp.api") + "/rtb/v2"; } - /** - * Modify request's first imp. - */ - private BidRequest modifyRequest(BidRequest request, ExtImpTappx extImpTappx) { - final List modifiedImps = new ArrayList<>(request.getImp()); - final BigDecimal extBidfloor = extImpTappx.getBidfloor(); - if (extBidfloor != null && extBidfloor.signum() > 0) { - final Imp modifiedFirstImp = request.getImp().get(0).toBuilder().bidfloor(extBidfloor).build(); - modifiedImps.set(0, modifiedFirstImp); - } + private static Imp modifyImp(Imp imp, ExtImpTappx extImpTappx) { + final BigDecimal extBidFloor = extImpTappx.getBidfloor(); + + return extBidFloor != null && extBidFloor.signum() == 1 + ? imp.toBuilder().bidfloor(extBidFloor).build() + : imp; + } - return request.toBuilder().imp(modifiedImps).ext(getExtRequest(extImpTappx)).build(); + private BidRequest modifyRequest(BidRequest request, List imps, ExtImpTappx extImpTappx) { + return request.toBuilder() + .imp(imps) + .ext(createRequestExt(extImpTappx)) + .build(); } - private ExtRequest getExtRequest(ExtImpTappx extImpTappx) { + private ExtRequest createRequestExt(ExtImpTappx extImpTappx) { + final TappxBidderExt tappxBidderExt = TappxBidderExt.builder() + .tappxkey(extImpTappx.getTappxkey()) + .mktag(extImpTappx.getMktag()) + .bcid(extImpTappx.getBcid()) + .bcrid(extImpTappx.getBcrid()) + .build(); + final ExtRequest extRequest = ExtRequest.empty(); - final TappxBidderExt tappxBidderExt = TappxBidderExt.of(extImpTappx.getTappxkey(), extImpTappx.getMktag(), - extImpTappx.getBcid(), extImpTappx.getBcrid()); extRequest.addProperty("bidder", mapper.mapper().valueToTree(tappxBidderExt)); - return extRequest; } + private HttpRequest makeHttpRequest(BidRequest request, String endpointUrl) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .headers(HttpUtil.headers()) + .uri(endpointUrl) + .body(mapper.encodeToBytes(request)) + .payload(request) + .build(); + } + @Override public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { try { diff --git a/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java b/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java index afe96df8855..ac9af4aa927 100644 --- a/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java +++ b/src/main/java/org/prebid/server/bidder/tappx/model/TappxBidderExt.java @@ -1,12 +1,12 @@ package org.prebid.server.bidder.tappx.model; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Value; import java.util.List; @Value -@AllArgsConstructor(staticName = "of") +@Builder public class TappxBidderExt { String tappxkey; diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 7898ed28e7e..688dc3a66f2 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -14,8 +14,8 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.CookieSyncEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.auction.model.CookieSyncContext; import org.prebid.server.bidder.BidderCatalog; diff --git a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java index 2a9dda4ef65..79c6d748758 100644 --- a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java +++ b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java @@ -12,9 +12,9 @@ import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.analytics.AnalyticsReporter; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.deals.UserService; import org.prebid.server.deals.events.ApplicationEventService; diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 361c3388433..1258c86c352 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -14,8 +14,8 @@ import lombok.Value; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.SetuidEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.auction.model.SetuidContext; import org.prebid.server.bidder.BidderCatalog; diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java index 9e7800e5291..2a07aa7f38f 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java @@ -21,8 +21,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java index aff7954e490..bd0fec649db 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java @@ -10,8 +10,8 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java index e8050e79d9a..c498086e0db 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -8,8 +8,8 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.RoutingContext; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; @@ -60,7 +60,7 @@ public class VideoHandler implements Handler { public VideoHandler(VideoRequestFactory videoRequestFactory, VideoResponseFactory videoResponseFactory, ExchangeService exchangeService, - CacheService cacheService, AnalyticsReporterDelegator analyticsDelegator, + CacheService cacheService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, Clock clock, PrebidVersionProvider prebidVersionProvider, diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java index c16b2d3177b..cf4ee8b2834 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java @@ -1,5 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request.tappx; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Value; @@ -12,8 +13,10 @@ public class ExtImpTappx { String host; + @JsonInclude(JsonInclude.Include.NON_EMPTY) String tappxkey; + @JsonInclude(JsonInclude.Include.NON_EMPTY) String endpoint; BigDecimal bidfloor; diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java index 4eb1e345414..180ce72421a 100644 --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java @@ -4,9 +4,10 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.prebid.server.analytics.AnalyticsReporter; -import org.prebid.server.analytics.AnalyticsReporterDelegator; -import org.prebid.server.analytics.LogAnalyticsReporter; -import org.prebid.server.analytics.pubstack.PubstackAnalyticsReporter; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.analytics.reporter.log.LogAnalyticsReporter; +import org.prebid.server.analytics.reporter.pubstack.PubstackAnalyticsReporter; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; @@ -50,12 +51,14 @@ LogAnalyticsReporter logAnalyticsReporter(JacksonMapper mapper) { public static class PubstackAnalyticsConfiguration { @Bean - PubstackAnalyticsReporter pubstackAnalyticsReporter(PubstackAnalyticsProperties pubstackAnalyticsProperties, - HttpClient httpClient, - JacksonMapper jacksonMapper, - Vertx vertx) { + PubstackAnalyticsReporter pubstackAnalyticsReporter( + PubstackAnalyticsConfiguratinProperties pubstackAnalyticsConfiguratinProperties, + HttpClient httpClient, + JacksonMapper jacksonMapper, + Vertx vertx) { + return new PubstackAnalyticsReporter( - pubstackAnalyticsProperties.toComponentProperties(), + pubstackAnalyticsConfiguratinProperties.toComponentProperties(), httpClient, jacksonMapper, vertx); @@ -63,14 +66,14 @@ PubstackAnalyticsReporter pubstackAnalyticsReporter(PubstackAnalyticsProperties @Bean @ConfigurationProperties(prefix = "analytics.pubstack") - PubstackAnalyticsProperties pubstackAnalyticsProperties() { - return new PubstackAnalyticsProperties(); + PubstackAnalyticsConfiguratinProperties pubstackAnalyticsConfiguratinProperties() { + return new PubstackAnalyticsConfiguratinProperties(); } @Validated @NoArgsConstructor @Data - private static class PubstackAnalyticsProperties { + private static class PubstackAnalyticsConfiguratinProperties { @NotNull String endpoint; @@ -89,8 +92,8 @@ private static class PubstackAnalyticsProperties { @NotNull PubstackBufferProperties buffers; - public org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties toComponentProperties() { - return org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties.builder() + public PubstackAnalyticsProperties toComponentProperties() { + return PubstackAnalyticsProperties.builder() .endpoint(getEndpoint()) .scopeId(getScopeid()) .enabled(getEnabled()) diff --git a/src/main/java/org/prebid/server/spring/config/WebConfiguration.java b/src/main/java/org/prebid/server/spring/config/WebConfiguration.java index 01b2565cb62..bee5f95e56d 100644 --- a/src/main/java/org/prebid/server/spring/config/WebConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/WebConfiguration.java @@ -13,7 +13,7 @@ import io.vertx.ext.web.handler.StaticHandler; import lombok.Data; import lombok.NoArgsConstructor; -import org.prebid.server.analytics.AnalyticsReporterDelegator; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.PrivacyEnforcementService; @@ -276,7 +276,7 @@ VideoHandler openrtbVideoHandler( videoRequestFactory, videoResponseFactory, exchangeService, - cacheService, analyticsReporter, + cacheService, analyticsReporter, metrics, clock, prebidVersionProvider, diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java index ebef82a62fb..846ad2f1057 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/TappxConfiguration.java @@ -14,6 +14,7 @@ import org.springframework.context.annotation.PropertySource; import javax.validation.constraints.NotBlank; +import java.time.Clock; @Configuration @PropertySource(value = "classpath:/bidder-config/tappx.yaml", factory = YamlPropertySourceFactory.class) @@ -30,12 +31,13 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps tappxBidderDeps(BidderConfigurationProperties tappxConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, + Clock clock, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(tappxConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new TappxBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new TappxBidder(config.getEndpoint(), clock, mapper)) .assemble(); } } diff --git a/src/main/resources/bidder-config/tappx.yaml b/src/main/resources/bidder-config/tappx.yaml index bc6209438aa..ee441fb458d 100644 --- a/src/main/resources/bidder-config/tappx.yaml +++ b/src/main/resources/bidder-config/tappx.yaml @@ -1,6 +1,6 @@ adapters: tappx: - endpoint: https:// + endpoint: https://{{subdomain}}.tappx.com meta-info: maintainer-email: tappx@tappx.com app-media-types: diff --git a/src/main/resources/static/bidder-params/tappx.json b/src/main/resources/static/bidder-params/tappx.json index 1d19fbeec22..6b83f86ed7d 100644 --- a/src/main/resources/static/bidder-params/tappx.json +++ b/src/main/resources/static/bidder-params/tappx.json @@ -39,5 +39,8 @@ } } }, - "required": ["host","tappxkey","endpoint"] + "required": [ + "tappxkey", + "endpoint" + ] } diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/Action.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/Action.groovy new file mode 100644 index 00000000000..8760760c00a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/alert/Action.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.deals.alert + +enum Action { + + RAISE +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertEvent.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertEvent.groovy new file mode 100644 index 00000000000..596b7221c46 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertEvent.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.deals.alert + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import java.time.ZonedDateTime + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class AlertEvent { + + String id + Action action + AlertPriority priority + ZonedDateTime updatedAt + String name + String details + AlertSource source +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertPriority.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertPriority.groovy new file mode 100644 index 00000000000..502b2d5eb25 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertPriority.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.deals.alert + +enum AlertPriority { + + HIGH, MEDIUM, LOW +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertSource.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertSource.groovy new file mode 100644 index 00000000000..3175711c4be --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/alert/AlertSource.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.deals.alert + +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 AlertSource { + + String env + String dataCenter + String region + String system + String subSystem + String hostId +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/DeliverySchedule.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/DeliverySchedule.groovy new file mode 100644 index 00000000000..8b59632171b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/DeliverySchedule.groovy @@ -0,0 +1,37 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonFormat +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC +import static org.prebid.server.functional.model.deals.lineitem.LineItem.TIME_PATTERN + +@ToString(includeNames = true, ignoreNulls = true) +class DeliverySchedule { + + String planId + + @JsonFormat(pattern = TIME_PATTERN) + ZonedDateTime startTimeStamp + + @JsonFormat(pattern = TIME_PATTERN) + ZonedDateTime endTimeStamp + + @JsonFormat(pattern = TIME_PATTERN) + ZonedDateTime updatedTimeStamp + + Set tokens + + static getDefaultDeliverySchedule() { + new DeliverySchedule(planId: PBSUtils.randomString, + startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), + endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusDays(1), + updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), + tokens: [Token.defaultToken] + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/FrequencyCap.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/FrequencyCap.groovy new file mode 100644 index 00000000000..995ae1b9309 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/FrequencyCap.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.deals.lineitem + +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +import static PeriodType.DAY + +@ToString(includeNames = true, ignoreNulls = true) +class FrequencyCap { + + String fcapId + Integer count + Integer periods + String periodType + + static getDefaultFrequencyCap() { + new FrequencyCap(count: 1, + fcapId: PBSUtils.randomString, + periods: 1, + periodType: DAY + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItem.groovy new file mode 100644 index 00000000000..43051f0ab50 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItem.groovy @@ -0,0 +1,74 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonFormat +import groovy.transform.ToString +import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting +import org.prebid.server.functional.util.PBSUtils + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static LineItemStatus.ACTIVE +import static java.time.ZoneOffset.UTC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.VERY_HIGH + +@ToString(includeNames = true, ignoreNulls = true) +class LineItem { + + public static final String TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'" + + String lineItemId + + String extLineItemId + + String dealId + + List sizes + + String accountId + + String source + + Price price + + RelativePriority relativePriority + + @JsonFormat(pattern = TIME_PATTERN) + ZonedDateTime startTimeStamp + + @JsonFormat(pattern = TIME_PATTERN) + ZonedDateTime endTimeStamp + + @JsonFormat(pattern = TIME_PATTERN) + ZonedDateTime updatedTimeStamp + + LineItemStatus status + + List frequencyCaps + + List deliverySchedules + + Targeting targeting + + static LineItem getDefaultLineItem(String accountId) { + int plannerAdapterLineItemId = PBSUtils.randomNumber + String plannerAdapterName = PBSUtils.randomString + new LineItem(lineItemId: "${plannerAdapterName}-$plannerAdapterLineItemId", + extLineItemId: plannerAdapterLineItemId, + dealId: PBSUtils.randomString, + sizes: [LineItemSize.defaultLineItemSize], + accountId: accountId, + source: GENERIC.name().toLowerCase(), + price: Price.defaultPrice, + relativePriority: VERY_HIGH, + startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), + endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusMonths(1), + updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), + status: ACTIVE, + frequencyCaps: [], + deliverySchedules: [DeliverySchedule.defaultDeliverySchedule], + targeting: Targeting.defaultTargeting + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemSize.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemSize.groovy new file mode 100644 index 00000000000..2ff7af18dec --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemSize.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.deals.lineitem + +import groovy.transform.ToString + +@ToString(includeNames = true) +class LineItemSize { + + Integer w + Integer h + + static getDefaultLineItemSize() { + new LineItemSize(w: 300, + h: 250 + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemStatus.groovy new file mode 100644 index 00000000000..c2dded2d3d2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/LineItemStatus.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonValue + +enum LineItemStatus { + + ACTIVE("active"), + DELETED("deleted"), + PAUSED("paused") + + @JsonValue + final String value + + private LineItemStatus(String value) { + this.value = value + } + + @Override + String toString() { + value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/MediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/MediaType.groovy new file mode 100644 index 00000000000..949da3d9b53 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/MediaType.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonValue + +enum MediaType { + + BANNER("banner") + + @JsonValue + final String value + + private MediaType(String value) { + this.value = value + } + + @Override + String toString() { + value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/PeriodType.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/PeriodType.groovy new file mode 100644 index 00000000000..10ca6f59d9c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/PeriodType.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonValue + +enum PeriodType { + + HOUR("hour"), + DAY("day"), + WEEK("week"), + MONTH("month"), + CAMPAIGN("campaign") + + @JsonValue + final String value + + private PeriodType(String value) { + this.value = value + } + + @Override + String toString() { + value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Price.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Price.groovy new file mode 100644 index 00000000000..f74f10f8292 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Price.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.deals.lineitem + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Price { + + BigDecimal cpm + String currency + + static getDefaultPrice() { + new Price(cpm: 0.01, + currency: "USD" + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/RelativePriority.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/RelativePriority.groovy new file mode 100644 index 00000000000..911da0b365f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/RelativePriority.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonValue + +enum RelativePriority { + + VERY_HIGH(1), + HIGH(2), + MEDIUM(3), + LOW(4), + VERY_LOW(5) + + @JsonValue + final Integer value + + private RelativePriority(Integer value) { + this.value = value + } + + @Override + String toString() { + value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Token.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Token.groovy new file mode 100644 index 00000000000..e7dd3f2fc5d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/Token.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.deals.lineitem + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Token { + + @JsonProperty("class") + Integer priorityClass + + Integer total + + static getDefaultToken() { + new Token(priorityClass: 1, + total: 1000 + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/BooleanOperator.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/BooleanOperator.groovy new file mode 100644 index 00000000000..0e4a4740e53 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/BooleanOperator.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.deals.lineitem.targeting + +import com.fasterxml.jackson.annotation.JsonValue + +enum BooleanOperator { + + AND('$and'), + OR('$or'), + NOT('$not'), + + INVALID('$invalid'), + UPPERCASE_AND('$AND') + + @JsonValue + final String value + + private BooleanOperator(String value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunction.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunction.groovy new file mode 100644 index 00000000000..54a1353808e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunction.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.deals.lineitem.targeting + +import com.fasterxml.jackson.annotation.JsonValue + +enum MatchingFunction { + + MATCHES('$matches'), + IN('$in'), + INTERSECTS('$intersects'), + WITHIN('$within') + + @JsonValue + final String value + + private MatchingFunction(String value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunctionNode.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunctionNode.groovy new file mode 100644 index 00000000000..6e639fe4383 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/MatchingFunctionNode.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.deals.lineitem.targeting + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.PackageScope + +@PackageScope +class MatchingFunctionNode { + + Map> matchingFunctionMultipleValuesNode + + Map matchingFunctionSingleValueNode + + @JsonValue + def getMatchingFunctionNode() { + matchingFunctionMultipleValuesNode ?: matchingFunctionSingleValueNode + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/Targeting.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/Targeting.groovy new file mode 100644 index 00000000000..12c9633cbaf --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/Targeting.groovy @@ -0,0 +1,92 @@ +package org.prebid.server.functional.model.deals.lineitem.targeting + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.deals.lineitem.LineItemSize + +import static BooleanOperator.AND +import static MatchingFunction.INTERSECTS +import static TargetingType.AD_UNIT_MEDIA_TYPE +import static TargetingType.AD_UNIT_SIZE +import static org.prebid.server.functional.model.deals.lineitem.MediaType.BANNER +import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.NOT +import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.OR + +class Targeting { + + private final Map> rootNode + + private final Map singleTargetingRootNode + + @JsonValue + def getSerializableRootNode() { + rootNode ?: singleTargetingRootNode + } + + private Targeting(Builder builder) { + rootNode = [(builder.rootOperator): builder.targetingNodes] + } + + private Targeting(Builder builder, TargetingNode targetingNode) { + singleTargetingRootNode = [(builder.rootOperator): targetingNode] + } + + Map> getTargetingRootNode() { + rootNode.asImmutable() + } + + static Targeting getDefaultTargeting() { + defaultTargetingBuilder.build() + } + + static Builder getDefaultTargetingBuilder() { + new Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) + } + + static Targeting getInvalidTwoRootNodesTargeting() { + defaultTargeting.tap { rootNode.put(OR, []) } + } + + static class Builder { + + private BooleanOperator rootOperator + private List targetingNodes = [] + + Builder(BooleanOperator rootOperator = AND) { + this.rootOperator = rootOperator + } + + Builder addTargeting(TargetingType targetingType, + MatchingFunction matchingFunction, + List targetingValues) { + MatchingFunctionNode matchingFunctionNode = new MatchingFunctionNode(matchingFunctionMultipleValuesNode: [(matchingFunction): targetingValues]) + addTargetingNode(targetingType, matchingFunctionNode) + this + } + + Builder addTargeting(TargetingType targetingType, + MatchingFunction matchingFunction, + Object targetingValue) { + MatchingFunctionNode matchingFunctionNode = new MatchingFunctionNode(matchingFunctionSingleValueNode: [(matchingFunction): targetingValue]) + addTargetingNode(targetingType, matchingFunctionNode) + this + } + + private void addTargetingNode(TargetingType targetingType, + MatchingFunctionNode matchingFunctionNode) { + targetingNodes << new TargetingNode([(targetingType): matchingFunctionNode]) + } + + Targeting build() { + new Targeting(this) + } + + Targeting buildNotBooleanOperatorTargeting(TargetingType targetingType, + MatchingFunction matchingFunction, + List targetingValues) { + rootOperator = NOT + MatchingFunctionNode matchingFunctionNode = new MatchingFunctionNode(matchingFunctionSingleValueNode: [(matchingFunction): targetingValues]) + new Targeting(this, new TargetingNode([(targetingType): matchingFunctionNode])) + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingNode.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingNode.groovy new file mode 100644 index 00000000000..4e8ccecd318 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingNode.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.deals.lineitem.targeting + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.PackageScope +import groovy.transform.TupleConstructor + +@PackageScope +@TupleConstructor +class TargetingNode { + + @JsonValue + Map targetingNode +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingType.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingType.groovy new file mode 100644 index 00000000000..a9e72a78dac --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/lineitem/targeting/TargetingType.groovy @@ -0,0 +1,45 @@ +package org.prebid.server.functional.model.deals.lineitem.targeting + +import com.fasterxml.jackson.annotation.JsonValue + +enum TargetingType { + + AD_UNIT_SIZE("adunit.size"), + AD_UNIT_MEDIA_TYPE("adunit.mediatype"), + AD_UNIT_AD_SLOT("adunit.adslot"), + SITE_DOMAIN("site.domain"), + SITE_PUBLISHER_DOMAIN("site.publisher.domain"), + REFERRER("site.referrer"), + APP_BUNDLE("app.bundle"), + DEVICE_COUNTRY("device.geo.ext.netacuity.country"), + DEVICE_TYPE("device.ext.deviceatlas.type"), + DEVICE_OS("device.ext.deviceatlas.osfamily"), + DEVICE_REGION("device.geo.ext.netacuity.region"), + PAGE_POSITION("pos"), + LOCATION("geo.distance"), + BIDP("bidp."), + BIDP_ACCOUNT_ID(BIDP.value + "rubicon.accountId"), + USER_SEGMENT("segment."), + USER_SEGMENT_NAME(USER_SEGMENT.value + "name"), + UFPD("ufpd."), + UFPD_LANGUAGE(UFPD.value + "language"), + UFPD_KEYWORDS(UFPD.value + "keywords"), + UFPD_BUYER_ID(UFPD.value + "buyerid"), + UFPD_BUYER_IDS(UFPD.value + "buyerids"), + SFPD("sfpd."), + SFPD_AMP(SFPD.value + "amp"), + SFPD_LANGUAGE(SFPD.value + "language"), + SFPD_KEYWORDS(SFPD.value + "keywords"), + SFPD_BUYER_ID(SFPD.value + "buyerid"), + SFPD_BUYER_IDS(SFPD.value + "buyerids"), + DOW("user.ext.time.userdow"), + HOUR("user.ext.time.userhour"), + INVALID("invalid.targeting.type") + + @JsonValue + final String value + + private TargetingType(String value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/register/CurrencyServiceState.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/register/CurrencyServiceState.groovy new file mode 100644 index 00000000000..4c0db24acd3 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/register/CurrencyServiceState.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.deals.register + +import groovy.transform.ToString + +import java.time.ZonedDateTime + +@ToString(includeNames = true, ignoreNulls = true) +class CurrencyServiceState { + + ZonedDateTime lastUpdate +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/register/RegisterRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/register/RegisterRequest.groovy new file mode 100644 index 00000000000..75daf33adc3 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/register/RegisterRequest.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.deals.register + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class RegisterRequest { + + BigDecimal healthIndex + Status status + String hostInstanceId + String region + String vendor +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/register/Status.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/register/Status.groovy new file mode 100644 index 00000000000..ad065e9ac4a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/register/Status.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.deals.register + +import groovy.transform.ToString +import org.prebid.server.functional.model.deals.report.DeliveryStatisticsReport + +@ToString(includeNames = true, ignoreNulls = true) +class Status { + + CurrencyServiceState currencyRates + DeliveryStatisticsReport dealsStatus +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliverySchedule.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliverySchedule.groovy new file mode 100644 index 00000000000..5701757e569 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliverySchedule.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.deals.report + +import groovy.transform.ToString + +import java.time.ZonedDateTime + +@ToString(includeNames = true, ignoreNulls = true) +class DeliverySchedule { + + String planId + ZonedDateTime planStartTimeStamp + ZonedDateTime planExpirationTimeStamp + ZonedDateTime planUpdatedTimeStamp + Set tokens +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliveryStatisticsReport.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliveryStatisticsReport.groovy new file mode 100644 index 00000000000..5ee7340c3dd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/report/DeliveryStatisticsReport.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.deals.report + +import groovy.transform.ToString + +import java.time.ZonedDateTime + +@ToString(includeNames = true, ignoreNulls = true) +class DeliveryStatisticsReport { + + String reportId + String instanceId + String vendor + String region + Long clientAuctions + Set lineItemStatus + ZonedDateTime reportTimeStamp + ZonedDateTime dataWindowStartTimeStamp + ZonedDateTime dataWindowEndTimeStamp +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/Event.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/Event.groovy new file mode 100644 index 00000000000..495bfaf673e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/report/Event.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.deals.report + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Event { + + String type + Long count +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatus.groovy new file mode 100644 index 00000000000..a68029852c6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/report/LineItemStatus.groovy @@ -0,0 +1,30 @@ +package org.prebid.server.functional.model.deals.report + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class LineItemStatus { + + String lineItemSource + String lineItemId + String dealId + String extLineItemId + Long accountAuctions + Long domainMatched + Long targetMatched + Long targetMatchedButFcapped + Long targetMatchedButFcapLookupFailed + Long pacingDeferred + Long sentToBidder + Long sentToBidderAsTopMatch + Long receivedFromBidder + Long receivedFromBidderInvalidated + Long sentToClient + Long sentToClientAsTopMatch + Set lostToLineItems + Set events + Set deliverySchedule + String readyAt + Long spentTokens + Long pacingFrequency +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/LostToLineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/LostToLineItem.groovy new file mode 100644 index 00000000000..58567ffc974 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/report/LostToLineItem.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.deals.report + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class LostToLineItem { + + String lineItemSource + String lineItemId + Long count +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/report/Token.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/report/Token.groovy new file mode 100644 index 00000000000..89310a5d73e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/report/Token.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.deals.report + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Token { + + @JsonProperty("class") + Integer priorityClass + + Integer total + + Long spent + + Long totalSpent +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/Segment.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/Segment.groovy new file mode 100644 index 00000000000..f11d8f32ff1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/Segment.groovy @@ -0,0 +1,14 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +class Segment { + + String id + + static getDefaultSegment() { + new Segment(id: PBSUtils.randomString) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/User.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/User.groovy new file mode 100644 index 00000000000..6d2e527a0e6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/User.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class User { + + List data + UserExt ext + + static getDefaultUser() { + new User(data: [UserData.defaultUserData], + ext: UserExt.defaultUserExt + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserData.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserData.groovy new file mode 100644 index 00000000000..5687d89285e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserData.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +class UserData { + + String id + String name + List segment + + static UserData getDefaultUserData() { + new UserData(id: PBSUtils.randomString, + name: PBSUtils.randomString, + segment: [Segment.defaultSegment] + ) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetails.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetails.groovy new file mode 100644 index 00000000000..780ec674b24 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetails.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class UserDetails { + + List userData + List fcapIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsRequest.groovy new file mode 100644 index 00000000000..74985c5808f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsRequest.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString + +import java.time.ZonedDateTime + +@ToString(includeNames = true, ignoreNulls = true) +class UserDetailsRequest { + + ZonedDateTime time + List ids +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsResponse.groovy new file mode 100644 index 00000000000..143aee234d9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserDetailsResponse.groovy @@ -0,0 +1,14 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString +import org.prebid.server.functional.model.ResponseModel + +@ToString(includeNames = true, ignoreNulls = true) +class UserDetailsResponse implements ResponseModel { + + User user + + static UserDetailsResponse getDefaultUserResponse(User user = User.defaultUser) { + new UserDetailsResponse(user: user) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserExt.groovy new file mode 100644 index 00000000000..53a89aa97e2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserExt.groovy @@ -0,0 +1,14 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +class UserExt { + + List fcapIds + + static getDefaultUserExt() { + new UserExt(fcapIds: [PBSUtils.randomString]) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserId.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserId.groovy new file mode 100644 index 00000000000..8edeac5336d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/UserId.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class UserId { + + String type + String id +} diff --git a/src/test/groovy/org/prebid/server/functional/model/deals/userdata/WinEventNotification.groovy b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/WinEventNotification.groovy new file mode 100644 index 00000000000..bea8f7c296b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/deals/userdata/WinEventNotification.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.deals.userdata + +import groovy.transform.ToString +import org.prebid.server.functional.model.deals.lineitem.FrequencyCap + +import java.time.ZonedDateTime + +@ToString(includeNames = true, ignoreNulls = true) +class WinEventNotification { + + String bidderCode + String bidId + String lineItemId + String region + List userIds + ZonedDateTime winEventDateTime + ZonedDateTime lineUpdatedDateTime + List frequencyCaps +} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/currencyconversion/CurrencyConversionRatesResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/currencyconversion/CurrencyConversionRatesResponse.groovy new file mode 100644 index 00000000000..6c469ff5cbf --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/mock/services/currencyconversion/CurrencyConversionRatesResponse.groovy @@ -0,0 +1,26 @@ +package org.prebid.server.functional.model.mock.services.currencyconversion + +import org.prebid.server.functional.model.ResponseModel + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC + +class CurrencyConversionRatesResponse implements ResponseModel { + + String dataAsOf + Map> conversions + + static CurrencyConversionRatesResponse getDefaultCurrencyConversionRatesResponse() { + new CurrencyConversionRatesResponse().tap { + dataAsOf = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(1) as String + conversions = defaultConversionRates + } + } + + private static getDefaultConversionRates() { + ["USD": ["EUR": 0.8872327211427558], + "EUR": ["USD": 1.3429368029739777]] as Map> + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/generalplanner/PlansResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/generalplanner/PlansResponse.groovy new file mode 100644 index 00000000000..f9a9d4548e7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/mock/services/generalplanner/PlansResponse.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.mock.services.generalplanner + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.ResponseModel +import org.prebid.server.functional.model.deals.lineitem.LineItem + +class PlansResponse implements ResponseModel { + + List lineItems + + static PlansResponse getDefaultPlansResponse(String accountId) { + new PlansResponse(lineItems: [LineItem.getDefaultLineItem(accountId)]) + } + + @JsonValue + List getLineItems() { + lineItems + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy index b4b8db9c7ae..99aeb50d2c7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/amp/AmpRequest.groovy @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils -import org.prebid.server.functional.util.privacy.ConsentString @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy index bff2f7e0cd9..afe7d82a65e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/App.groovy @@ -20,6 +20,7 @@ class App { Publisher publisher Content content String keywords + AppExt ext static App getDefaultApp() { new App(id: PBSUtils.randomString) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy new file mode 100644 index 00000000000..b31926c14b5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AppExt { + + AppExtData data +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExtData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExtData.groovy new file mode 100644 index 00000000000..3d12506410c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExtData.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AppExtData { + + String language +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy index bb8b86ab594..487a26968da 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy @@ -11,4 +11,5 @@ class Deal { Integer at List wseat List wadomain + DealExt ext } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy new file mode 100644 index 00000000000..90b57aa8fb9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true) +class DealExt { + + DealLineItem line +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy new file mode 100644 index 00000000000..4ab7823193c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class DealLineItem { + + String lineItemId + String extLineItemId + List sizes + String bidder +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy index 5538572912e..91616f2b7f6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy @@ -16,6 +16,7 @@ class ImpExt { @Deprecated @JsonProperty("appnexus") AppNexus appNexus + ImpExtContext context static ImpExt getDefaultImpExt() { new ImpExt().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy new file mode 100644 index 00000000000..bff08aa290b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContext.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class ImpExtContext { + + ImpExtContextData data +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy new file mode 100644 index 00000000000..f63b32f2833 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtContextData.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class ImpExtContextData { + + String language + List keywords + Integer buyerid + List buyerids +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PgMetrics.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PgMetrics.groovy new file mode 100644 index 00000000000..50684b621c3 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PgMetrics.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class PgMetrics { + + Set sentToClient + Set sentToClientAsTopMatch + Set matchedDomainTargeting + Set matchedWholeTargeting + Set matchedTargetingFcapped + Set matchedTargetingFcapLookupFailed + Set readyToServe + Set pacingDeferred + Map> sentToBidder + Map> sentToBidderAsTopMatch + Map> receivedFromBidder + Set responseInvalidated +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy index bd5f64fbe82..ce72554490b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy @@ -1,8 +1,11 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class Pmp { Integer privateAuction diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index faaa43d5f5d..08878e0e9dc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -15,6 +15,7 @@ class Prebid { PrebidStoredRequest storedRequest Amp amp Channel channel + Map aliases List schains List multibid Pbs pbs diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExt.groovy index 2a9f35a6d44..126d7f11fd7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExt.groovy @@ -6,4 +6,5 @@ import groovy.transform.ToString class SiteExt { Integer amp + SiteExtData data } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy new file mode 100644 index 00000000000..09b545b2f32 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class SiteExtData { + + String language +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy index 8df6e8039d6..4f7a24c5bef 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/User.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.request.auction import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) class User { @@ -15,4 +16,8 @@ class User { Geo geo List data UserExt ext + + static getDefaultUser() { + new User(id: PBSUtils.randomString) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy index 4ccee84ff0e..9e51f41fd18 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy @@ -7,4 +7,7 @@ import org.prebid.server.functional.util.privacy.ConsentString class UserExt { ConsentString consent + List fcapids + UserTime time + UserExtData data } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExtData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExtData.groovy new file mode 100644 index 00000000000..56d69f1e4ce --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExtData.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class UserExtData { + + String language + List keywords + Integer buyerid + List buyerids +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserTime.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserTime.groovy new file mode 100644 index 00000000000..bc81ab3fc6f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserTime.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class UserTime { + + Integer userdow + Integer userhour +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/dealsupdate/ForceDealsUpdateRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/dealsupdate/ForceDealsUpdateRequest.groovy new file mode 100644 index 00000000000..aaa1c0b2ece --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/dealsupdate/ForceDealsUpdateRequest.groovy @@ -0,0 +1,48 @@ +package org.prebid.server.functional.model.request.dealsupdate + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.CREATE_REPORT +import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.INVALIDATE_LINE_ITEMS +import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.REGISTER_INSTANCE +import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.RESET_ALERT_COUNT +import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.SEND_REPORT +import static org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest.Action.UPDATE_LINE_ITEMS + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class ForceDealsUpdateRequest { + + String actionName + + static ForceDealsUpdateRequest getUpdateLineItemsRequest() { + new ForceDealsUpdateRequest(actionName: UPDATE_LINE_ITEMS.name()) + } + + static ForceDealsUpdateRequest getSendReportRequest() { + new ForceDealsUpdateRequest(actionName: SEND_REPORT.name()) + } + + static ForceDealsUpdateRequest getRegisterInstanceRequest() { + new ForceDealsUpdateRequest(actionName: REGISTER_INSTANCE.name()) + } + + static ForceDealsUpdateRequest getResetAlertCountRequest() { + new ForceDealsUpdateRequest(actionName: RESET_ALERT_COUNT.name()) + } + + static ForceDealsUpdateRequest getCreateReportRequest() { + new ForceDealsUpdateRequest(actionName: CREATE_REPORT.name()) + } + + static ForceDealsUpdateRequest getInvalidateLineItemsRequest() { + new ForceDealsUpdateRequest(actionName: INVALIDATE_LINE_ITEMS.name()) + } + + private enum Action { + + UPDATE_LINE_ITEMS, SEND_REPORT, REGISTER_INSTANCE, RESET_ALERT_COUNT, CREATE_REPORT, INVALIDATE_LINE_ITEMS + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy index 6ed94f08161..9d7000cb7b3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/event/EventRequest.groovy @@ -24,6 +24,8 @@ class EventRequest { Integer analytics @JsonProperty("ts") Long timestamp + @JsonProperty("l") + String lineItemId static EventRequest getDefaultEventRequest() { def request = new EventRequest() diff --git a/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy b/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy index 89de6fef872..4f5fc3cd2e3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/Debug.groovy @@ -2,8 +2,9 @@ package org.prebid.server.functional.model.response import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.model.response.auction.HttpCall +import org.prebid.server.functional.model.request.auction.PgMetrics import org.prebid.server.functional.model.response.auction.DebugPrivacy +import org.prebid.server.functional.model.response.auction.HttpCall @ToString(includeNames = true, ignoreNulls = true) class Debug { @@ -11,6 +12,7 @@ class Debug { Map> httpcalls BidRequest resolvedrequest DebugPrivacy privacy + PgMetrics pgmetrics Map> getBidders() { def result = httpcalls?.findAll { it.key != "cache" } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy index 2398e99d576..16d50a6c085 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponse.groovy @@ -3,6 +3,7 @@ package org.prebid.server.functional.model.response.auction import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.ResponseModel +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse import org.prebid.server.functional.model.request.auction.BidRequest @EqualsAndHashCode @@ -29,6 +30,16 @@ class BidResponse implements ResponseModel { bidResponse } + static BidResponse getDefaultPgBidResponse(BidRequest bidRequest, PlansResponse plansResponse) { + def bidResponse = getDefaultBidResponse(bidRequest) + def bid = bidResponse.seatbid[0].bid[0] + def lineItem = plansResponse.lineItems[0] + bid.dealid = lineItem.dealId + bid.w = lineItem.sizes[0].w + bid.h = lineItem.sizes[0].h + bidResponse + } + static private List getDefaultBids(List impIds) { impIds.collect { Bid.getDefaultBid(it) } } 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 783bdc70ad4..2d7ba43ba10 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -12,6 +12,7 @@ import org.prebid.server.functional.model.mock.services.prebidcache.response.Pre 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.cookiesync.CookieSyncRequest +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest import org.prebid.server.functional.model.request.event.EventRequest import org.prebid.server.functional.model.request.logging.httpinteraction.HttpInteractionRequest import org.prebid.server.functional.model.request.setuid.SetuidRequest @@ -55,6 +56,7 @@ class PrebidServerService { static final String CURRENCY_RATES_ENDPOINT = "/currency/rates" static final String HTTP_INTERACTION_ENDPOINT = "/logging/httpinteraction" static final String COLLECTED_METRICS_ENDPOINT = "/collected-metrics" + static final String FORCE_DEALS_UPDATE_ENDPOINT = "/pbs-admin/force-deals-update" private final PrebidServerContainer pbsContainer private final ObjectMapperWrapper mapper @@ -165,8 +167,9 @@ class PrebidServerService { } @Step("[GET] /event") - byte[] sendEventRequest(EventRequest eventRequest) { - def response = given(requestSpecification).queryParams(mapper.toMap(eventRequest)) + byte[] sendEventRequest(EventRequest eventRequest, Map headers = [:]) { + def response = given(requestSpecification).headers(headers) + .queryParams(mapper.toMap(eventRequest)) .get(EVENT_ENDPOINT) checkResponseStatusCode(response) @@ -250,6 +253,14 @@ class PrebidServerService { mapper.decode(response.asString(), new TypeReference>() {}) } + @Step("[GET] /pbs-admin/force-deals-update") + void sendForceDealsUpdateRequest(ForceDealsUpdateRequest forceDealsUpdateRequest) { + def response = given(adminRequestSpecification).queryParams(mapper.toMap(forceDealsUpdateRequest)) + .get(FORCE_DEALS_UPDATE_ENDPOINT) + + checkResponseStatusCode(response, 204) + } + private Response postAuction(BidRequest bidRequest, Map headers = [:]) { def payload = mapper.encode(bidRequest) @@ -264,12 +275,12 @@ class PrebidServerService { .get(AMP_ENDPOINT) } - private void checkResponseStatusCode(Response response) { - def statusCode = response.statusCode - if (statusCode != 200) { + private void checkResponseStatusCode(Response response, int statusCode = 200) { + def responseStatusCode = response.statusCode + if (responseStatusCode != statusCode) { def responseBody = response.body.asString() log.error(responseBody) - throw new PrebidServerException(statusCode, responseBody, getHeaders(response)) + throw new PrebidServerException(responseStatusCode, responseBody, getHeaders(response)) } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsPgConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsPgConfig.groovy new file mode 100644 index 00000000000..4c55df2651f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsPgConfig.groovy @@ -0,0 +1,130 @@ +package org.prebid.server.functional.testcontainers + +import org.prebid.server.functional.testcontainers.container.NetworkServiceContainer +import org.prebid.server.functional.util.PBSUtils + +import java.time.LocalDate + +import static org.prebid.server.functional.testcontainers.scaffolding.pg.Alert.ALERT_ENDPOINT_PATH +import static org.prebid.server.functional.testcontainers.scaffolding.pg.DeliveryStatistics.REPORT_DELIVERY_ENDPOINT_PATH +import static org.prebid.server.functional.testcontainers.scaffolding.pg.GeneralPlanner.PLANS_ENDPOINT_PATH +import static org.prebid.server.functional.testcontainers.scaffolding.pg.GeneralPlanner.REGISTER_ENDPOINT_PATH +import static org.prebid.server.functional.testcontainers.scaffolding.pg.UserData.USER_DETAILS_ENDPOINT_PATH +import static org.prebid.server.functional.testcontainers.scaffolding.pg.UserData.WIN_EVENT_ENDPOINT_PATH + +class PbsPgConfig { + + public static final String PG_ENDPOINT_USERNAME = "pg" + public static final String PG_ENDPOINT_PASSWORD = "pg" + + private static final int NEXT_MONTH = LocalDate.now().plusMonths(1).monthValue + + final Map properties + final String env + final String dataCenter + final String region + final String system + final String subSystem + final String hostId + final String vendor + final String currency + final String userIdType + final int maxDealsPerBidder + final int lineItemsPerReport + + PbsPgConfig(NetworkServiceContainer networkServiceContainer) { + properties = getPgConfig(networkServiceContainer.rootUri).asImmutable() + env = properties.get("profile") + dataCenter = properties.get("data-center") + region = properties.get("datacenter-region") + system = properties.get("system") + subSystem = properties.get("sub-system") + hostId = properties.get("host-id") + vendor = properties.get("vendor") + currency = properties.get("auction.ad-server-currency") + userIdType = properties.get("deals.user-data.user-ids[0].type") + maxDealsPerBidder = getIntProperty(properties, "deals.max-deals-per-bidder") + lineItemsPerReport = getIntProperty(properties, "deals.delivery-stats.line-items-per-report") + } + + private static Map getPgConfig(String networkServiceContainerUri) { + pbsGeneralSettings() + adminDealsUpdateEndpoint() + deals() + deliveryProgress() + + planner(networkServiceContainerUri) + deliveryStatistics(networkServiceContainerUri) + + alert(networkServiceContainerUri) + userData(networkServiceContainerUri) + } + + private static Map pbsGeneralSettings() { + ["host-id" : PBSUtils.randomString, + "datacenter-region" : PBSUtils.randomString, + "vendor" : PBSUtils.randomString, + "profile" : PBSUtils.randomString, + "system" : PBSUtils.randomString, + "sub-system" : PBSUtils.randomString, + "data-center" : PBSUtils.randomString, + "auction.ad-server-currency": "USD", + ] + } + + private static Map adminDealsUpdateEndpoint() { + ["admin-endpoints.force-deals-update.enabled": "true"] + } + + private static Map deals() { + ["deals.enabled" : "true", + "deals.simulation.enabled" : "false", + "deals.max-deals-per-bidder": "3" + ] + } + + private static Map planner(String networkServiceContainerUri) { + ["deals.planner.plan-endpoint" : networkServiceContainerUri + PLANS_ENDPOINT_PATH, + "deals.planner.register-endpoint" : networkServiceContainerUri + REGISTER_ENDPOINT_PATH, + "deals.planner.update-period" : "0 15 10 15 $NEXT_MONTH ?" as String, + "deals.planner.plan-advance-period": "0 15 10 15 $NEXT_MONTH ?" as String, + "deals.planner.timeout-ms" : "5000", + "deals.planner.username" : PG_ENDPOINT_USERNAME, + "deals.planner.password" : PG_ENDPOINT_PASSWORD, + "deals.planner.register-period-sec": "3600" + ] + } + + private static Map deliveryStatistics(String networkServiceContainerUri) { + ["deals.delivery-stats.endpoint" : networkServiceContainerUri + + REPORT_DELIVERY_ENDPOINT_PATH, + "deals.delivery-stats.username" : PG_ENDPOINT_USERNAME, + "deals.delivery-stats.password" : PG_ENDPOINT_PASSWORD, + "deals.delivery-stats.delivery-period" : "0 15 10 15 $NEXT_MONTH ?" as String, + "deals.delivery-stats.timeout-ms" : "10000", + "deals.delivery-stats.request-compression-enabled": "false", + "deals.delivery-stats.line-items-per-report" : "5" + ] + } + + private static Map deliveryProgress() { + ["deals.delivery-progress.report-reset-period": "0 15 10 15 $NEXT_MONTH ?" as String] + } + + private static Map alert(String networkServiceContainerUri) { + ["deals.alert-proxy.enabled" : "true", + "deals.alert-proxy.url" : networkServiceContainerUri + ALERT_ENDPOINT_PATH, + "deals.alert-proxy.username" : PG_ENDPOINT_USERNAME, + "deals.alert-proxy.password" : PG_ENDPOINT_PASSWORD, + "deals.alert-proxy.timeout-sec": "10" + ] + } + + private static Map userData(String networkServiceContainerUri) { + ["deals.user-data.user-details-endpoint": networkServiceContainerUri + USER_DETAILS_ENDPOINT_PATH, + "deals.user-data.win-event-endpoint" : networkServiceContainerUri + WIN_EVENT_ENDPOINT_PATH, + "deals.user-data.timeout" : "1000", + "deals.user-data.user-ids[0].type" : "autotest", + "deals.user-data.user-ids[0].source" : "uid", + "deals.user-data.user-ids[0].location" : "generic" + ] + } + + private static getIntProperty(Map properties, String propertyName) { + def property = properties.get(propertyName) + property ? property as int : -1 + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy index f78f93aa1b7..381a1b4471f 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy @@ -6,6 +6,7 @@ import org.prebid.server.functional.testcontainers.container.PrebidServerContain import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils +// TODO make container instance into a POGO and add the ability for any given container to live through stopContainers() class PbsServiceFactory { private static final Map, PrebidServerContainer> containers = [:] @@ -26,7 +27,7 @@ class PbsServiceFactory { remove([(container.key): container.value]) } if (containers.containsKey(config)) { - return new PrebidServerService(containers.get(config), mapper) + return new PrebidServerService(getContainer(config), mapper) } else { def pbsContainer = new PrebidServerContainer(config) pbsContainer.start() @@ -35,6 +36,10 @@ class PbsServiceFactory { } } + static PrebidServerContainer getContainer(Map config) { + containers.get(config) + } + static void stopContainers() { def containers = containers.findAll { it.key != [:] } remove(containers) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index 6136d727e99..ca857b48671 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -10,8 +10,8 @@ class PrebidServerContainer extends GenericContainer { public static final int PORT = 8080 public static final int DEBUG_PORT = 8000 public static final int ADMIN_PORT = 8060 - public static final String ADMIN_ENDPOINT_USERNAME = "user" - public static final String ADMIN_ENDPOINT_PASSWORD = "user" + public static final String ADMIN_ENDPOINT_USERNAME = "admin" + public static final String ADMIN_ENDPOINT_PASSWORD = "admin" public static final String APP_WORKDIR = "/app/prebid-server/" private static final String DB_ACCOUNT_QUERY = """ @@ -159,6 +159,8 @@ LIMIT 1 private static String normalizeProperty(String property) { property.replace(".", "_") .replace("-", "") + .replace("[", "_") + .replace("]", "_") } @Override diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy new file mode 100644 index 00000000000..775de7459bd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy @@ -0,0 +1,41 @@ +package org.prebid.server.functional.testcontainers.scaffolding + +import org.mockserver.model.HttpRequest +import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.MockServerContainer + +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.OK_200 + +class CurrencyConversion extends NetworkScaffolding { + + static final String CURRENCY_ENDPOINT_PATH = "/currency" + + CurrencyConversion(MockServerContainer mockServerContainer, ObjectMapperWrapper mapper) { + super(mockServerContainer, CURRENCY_ENDPOINT_PATH, mapper) + } + + void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse) { + setResponse(request, conversionRatesResponse) + } + + @Override + void setResponse() { + mockServerClient.when(request().withPath(endpoint)) + .respond(response().withStatusCode(OK_200.code())) + } + + @Override + protected HttpRequest getRequest(String ignored) { + request().withMethod("GET") + .withPath(CURRENCY_ENDPOINT_PATH) + } + + @Override + protected HttpRequest getRequest() { + request().withMethod("GET") + .withPath(CURRENCY_ENDPOINT_PATH) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy index 01d40a98ce8..4e2a80cfdad 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy @@ -2,12 +2,15 @@ package org.prebid.server.functional.testcontainers.scaffolding import org.mockserver.client.MockServerClient import org.mockserver.matchers.Times +import org.mockserver.model.ClearType import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode import org.prebid.server.functional.model.ResponseModel import org.prebid.server.functional.util.ObjectMapperWrapper import org.testcontainers.containers.MockServerContainer import static java.util.concurrent.TimeUnit.SECONDS +import static org.mockserver.model.ClearType.EXPECTATIONS import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 @@ -46,31 +49,13 @@ abstract class NetworkScaffolding { .size() } - boolean checkRequestCount(int expectedCount, int pollTime = 1000, int pollFrequency = 50) { - def expectedCountReached = false - def startTime = System.currentTimeMillis() - def elapsedTime = 0 - - while (elapsedTime < pollTime) { - def requestCount = getRequestCount() - if (requestCount == expectedCount) { - expectedCountReached = true - break - } else if (requestCount > expectedCount) { - throw new IllegalStateException("The number of recorded requests: $requestCount exceeds the expected number: $expectedCount") - } else { - elapsedTime += System.currentTimeMillis() - startTime - Thread.sleep(pollFrequency) - } - } - - expectedCountReached - } - - void setResponse(HttpRequest httpRequest, ResponseModel responseModel) { + void setResponse(HttpRequest httpRequest, + ResponseModel responseModel, + HttpStatusCode statusCode = OK_200, + Times times = Times.exactly(1)) { def mockResponse = mapper.encode(responseModel) - mockServerClient.when(httpRequest, Times.exactly(1)) - .respond(response().withStatusCode(OK_200.code()) + mockServerClient.when(httpRequest, times) + .respond(response().withStatusCode(statusCode.code()) .withBody(mockResponse, APPLICATION_JSON)) } @@ -135,8 +120,8 @@ abstract class NetworkScaffolding { getRequestsHeaders(mockServerClient.retrieveRecordedRequests(getRequest(value)) as List) } - void reset() { - mockServerClient.clear(request().withPath(endpoint)) + void reset(String resetEndpoint = endpoint, ClearType clearType = EXPECTATIONS) { + mockServerClient.clear(request().withPath(resetEndpoint), clearType) } private static List> getRequestsHeaders(List httpRequests) { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/Alert.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/Alert.groovy new file mode 100644 index 00000000000..2556f633481 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/Alert.groovy @@ -0,0 +1,51 @@ +package org.prebid.server.functional.testcontainers.scaffolding.pg + +import com.fasterxml.jackson.core.type.TypeReference +import org.mockserver.model.HttpRequest +import org.prebid.server.functional.model.deals.alert.AlertEvent +import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.MockServerContainer + +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.mockserver.model.JsonPathBody.jsonPath + +class Alert extends NetworkScaffolding { + + static final String ALERT_ENDPOINT_PATH = "/deals/alert" + + Alert(MockServerContainer mockServerContainer, ObjectMapperWrapper mapper) { + super(mockServerContainer, ALERT_ENDPOINT_PATH, mapper) + } + + AlertEvent getRecordedAlertRequest() { + def body = getRecordedRequestsBody(request).last() + // 0 index element is returned after deserialization as PBS responses with SingletonList + mapper.decode(body, new TypeReference>() {})[0] + } + + Map getLastRecordedAlertRequestHeaders() { + getLastRecordedRequestHeaders(request) + } + + @Override + void setResponse() { + mockServerClient.when(request().withPath(endpoint)) + .respond(response().withStatusCode(OK_200.code())) + } + + @Override + protected HttpRequest getRequest(String alertId) { + request().withMethod("POST") + .withPath(ALERT_ENDPOINT_PATH) + .withBody(jsonPath("\$[?(@.id == '$alertId')]")) + } + + @Override + protected HttpRequest getRequest() { + request().withMethod("POST") + .withPath(ALERT_ENDPOINT_PATH) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/DeliveryStatistics.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/DeliveryStatistics.groovy new file mode 100644 index 00000000000..b0b8d80b836 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/DeliveryStatistics.groovy @@ -0,0 +1,58 @@ +package org.prebid.server.functional.testcontainers.scaffolding.pg + +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.deals.report.DeliveryStatisticsReport +import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.MockServerContainer + +import static org.mockserver.model.ClearType.ALL +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.mockserver.model.JsonPathBody.jsonPath + +class DeliveryStatistics extends NetworkScaffolding { + + static final String REPORT_DELIVERY_ENDPOINT_PATH = "/deals/report/delivery" + + DeliveryStatistics(MockServerContainer mockServerContainer, ObjectMapperWrapper mapper) { + super(mockServerContainer, REPORT_DELIVERY_ENDPOINT_PATH, mapper) + } + + Map getLastRecordedDeliveryRequestHeaders() { + getLastRecordedRequestHeaders(request) + } + + DeliveryStatisticsReport getLastRecordedDeliveryStatisticsReportRequest() { + recordedDeliveryStatisticsReportRequests.last() + } + + void resetRecordedRequests() { + reset(REPORT_DELIVERY_ENDPOINT_PATH, ALL) + } + + void setResponse(HttpStatusCode statusCode = OK_200) { + mockServerClient.when(request().withPath(endpoint)) + .respond(response().withStatusCode(statusCode.code())) + } + + List getRecordedDeliveryStatisticsReportRequests() { + def body = getRecordedRequestsBody(request) + body.collect { mapper.decode(it, DeliveryStatisticsReport) } + } + + @Override + protected HttpRequest getRequest(String reportId) { + request().withMethod("POST") + .withPath(REPORT_DELIVERY_ENDPOINT_PATH) + .withBody(jsonPath("\$[?(@.reportId == '$reportId')]")) + } + + @Override + protected HttpRequest getRequest() { + request().withMethod("POST") + .withPath(REPORT_DELIVERY_ENDPOINT_PATH) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/GeneralPlanner.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/GeneralPlanner.groovy new file mode 100644 index 00000000000..03774bd49df --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/GeneralPlanner.groovy @@ -0,0 +1,91 @@ +package org.prebid.server.functional.testcontainers.scaffolding.pg + +import org.mockserver.matchers.Times +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.deals.register.RegisterRequest +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.MockServerContainer + +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.mockserver.model.JsonPathBody.jsonPath + +class GeneralPlanner extends NetworkScaffolding { + + static final String PLANS_ENDPOINT_PATH = "/deals/plans" + static final String REGISTER_ENDPOINT_PATH = "/deals/register" + + GeneralPlanner(MockServerContainer mockServerContainer, ObjectMapperWrapper mapper) { + super(mockServerContainer, REGISTER_ENDPOINT_PATH, mapper) + } + + void initRegisterResponse(HttpStatusCode statusCode = OK_200) { + reset() + setResponse(statusCode) + } + + void initPlansResponse(PlansResponse plansResponse, + HttpStatusCode statusCode = OK_200, + Times times = Times.exactly(1)) { + resetPlansEndpoint() + setPlansResponse(plansResponse, statusCode, times) + } + + void resetPlansEndpoint() { + reset(PLANS_ENDPOINT_PATH) + } + + int getRecordedPlansRequestCount() { + getRequestCount(plansRequest) + } + + RegisterRequest getLastRecordedRegisterRequest() { + recordedRegisterRequests.last() + } + + List getRecordedRegisterRequests() { + def body = getRecordedRequestsBody(request) + body.collect { mapper.decode(it, RegisterRequest) } + } + + void setResponse(HttpStatusCode statusCode = OK_200) { + mockServerClient.when(request().withPath(endpoint)) + .respond(response().withStatusCode(statusCode.code())) + } + + Map getLastRecordedRegisterRequestHeaders() { + getLastRecordedRequestHeaders(request) + } + + Map getLastRecordedPlansRequestHeaders() { + getLastRecordedRequestHeaders(plansRequest) + } + + private void setPlansResponse(PlansResponse plansResponse, + HttpStatusCode statusCode, + Times times = Times.exactly(1)) { + setResponse(plansRequest, plansResponse, statusCode, times) + } + + @Override + protected HttpRequest getRequest(String hostInstanceId) { + request().withMethod("POST") + .withPath(REGISTER_ENDPOINT_PATH) + .withBody(jsonPath("\$[?(@.hostInstanceId == '$hostInstanceId')]")) + } + + @Override + protected HttpRequest getRequest() { + request().withMethod("POST") + .withPath(REGISTER_ENDPOINT_PATH) + } + + private static HttpRequest getPlansRequest() { + request().withMethod("GET") + .withPath(PLANS_ENDPOINT_PATH) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/UserData.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/UserData.groovy new file mode 100644 index 00000000000..776c81b043c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/pg/UserData.groovy @@ -0,0 +1,72 @@ +package org.prebid.server.functional.testcontainers.scaffolding.pg + +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.deals.userdata.UserDetailsRequest +import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse +import org.prebid.server.functional.model.deals.userdata.WinEventNotification +import org.prebid.server.functional.testcontainers.scaffolding.NetworkScaffolding +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.MockServerContainer + +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.mockserver.model.JsonPathBody.jsonPath + +class UserData extends NetworkScaffolding { + + static final String USER_DETAILS_ENDPOINT_PATH = "/deals/user-details" + static final String WIN_EVENT_ENDPOINT_PATH = "/deals/win-event" + + UserData(MockServerContainer mockServerContainer, ObjectMapperWrapper mapper) { + super(mockServerContainer, WIN_EVENT_ENDPOINT_PATH, mapper) + } + + UserDetailsRequest getRecordedUserDetailsRequest() { + def body = getRecordedRequestsBody(userDetailsRequest).last() + mapper.decode(body, UserDetailsRequest) + } + + WinEventNotification getRecordedWinEventRequest() { + def body = getRecordedRequestsBody(request).last() + mapper.decode(body, WinEventNotification) + } + + void setUserDataResponse(UserDetailsResponse userDataResponse, HttpStatusCode httpStatusCode = OK_200) { + resetUserDetailsEndpoint() + setResponse(userDetailsRequest, userDataResponse, httpStatusCode) + } + + int getRecordedUserDetailsRequestCount() { + getRequestCount(userDetailsRequest) + } + + void resetUserDetailsEndpoint() { + reset(USER_DETAILS_ENDPOINT_PATH) + } + + @Override + void setResponse() { + mockServerClient.when(request().withPath(endpoint)) + .respond(response().withStatusCode(OK_200.code())) + } + + @Override + protected HttpRequest getRequest(String bidId) { + request().withMethod("POST") + .withPath(WIN_EVENT_ENDPOINT_PATH) + .withBody(jsonPath("\$[?(@.bidId == '$bidId')]")) + } + + @Override + protected HttpRequest getRequest() { + request().withMethod("POST") + .withPath(WIN_EVENT_ENDPOINT_PATH) + } + + private static HttpRequest getUserDetailsRequest() { + request().withMethod("POST") + .withPath(USER_DETAILS_ENDPOINT_PATH) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy index 6591643d3b4..8c87c5acdeb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy @@ -6,6 +6,7 @@ import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.Dependencies import org.prebid.server.functional.testcontainers.PBSTest import org.prebid.server.functional.testcontainers.scaffolding.PubStackAnalytics +import org.prebid.server.functional.util.PBSUtils import spock.lang.Ignore import spock.lang.Shared @@ -32,6 +33,6 @@ class AnalyticsSpec extends BaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should call pubstack analytics" - assert analytics.checkRequestCount(analyticsRequestCount + 1) + PBSUtils.waitUntil { analytics.requestCount == analyticsRequestCount + 1 } } } 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 2866e3a5619..6e33dd5fa0e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -19,7 +19,6 @@ import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED @PBSTest diff --git a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy index 86dec7566e2..692a0ffc0fb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy @@ -1,12 +1,12 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.mock.services.httpsettings.HttpAccountsResponse 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.event.EventRequest import org.prebid.server.functional.model.request.setuid.SetuidRequest -import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.service.PrebidServerException @@ -120,7 +120,8 @@ class HttpSettingsSpec extends BaseSpec { assert response.uidsCookie.bday assert !response.uidsCookie.tempUIDs assert !response.uidsCookie.uids - assert response.responseBody == ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") and: "There should be only one account request" assert httpSettings.getRequestCount(request.account) == 1 diff --git a/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy index 374dae4518c..63e6e584183 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SmokeSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.request.amp.AmpRequest @@ -8,7 +9,6 @@ import org.prebid.server.functional.model.request.cookiesync.CookieSyncRequest import org.prebid.server.functional.model.request.event.EventRequest import org.prebid.server.functional.model.request.logging.httpinteraction.HttpInteractionRequest import org.prebid.server.functional.model.request.setuid.SetuidRequest -import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.model.response.cookiesync.CookieSyncResponse diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/AlertSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/AlertSpec.groovy new file mode 100644 index 00000000000..1120a2aca48 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/AlertSpec.groovy @@ -0,0 +1,279 @@ +package org.prebid.server.functional.tests.pg + +import org.mockserver.matchers.Times +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Unroll + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 +import static org.mockserver.model.HttpStatusCode.NOT_FOUND_404 +import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 +import static org.prebid.server.functional.model.deals.alert.Action.RAISE +import static org.prebid.server.functional.model.deals.alert.AlertPriority.LOW +import static org.prebid.server.functional.model.deals.alert.AlertPriority.MEDIUM +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER +import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER +import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER_VALUE +import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER +import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX + +class AlertSpec extends BasePgSpec { + + private static final String PBS_REGISTER_CLIENT_ERROR = "pbs-register-client-error" + private static final String PBS_PLANNER_CLIENT_ERROR = "pbs-planner-client-error" + private static final String PBS_PLANNER_EMPTY_RESPONSE = "pbs-planner-empty-response-error" + private static final String PBS_DELIVERY_CLIENT_ERROR = "pbs-delivery-stats-client-error" + private static final Integer DEFAULT_ALERT_PERIOD = 15 + + def "PBS should send alert request when the threshold is reached"() { + given: "Changed Planner Register endpoint response to return bad status code" + generalPlanner.initRegisterResponse(NOT_FOUND_404) + + and: "PBS alert counter is reset" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) + + and: "Initial Alert Service request count is taken" + def initialRequestCount = alert.requestCount + + when: "Initiating PBS to register its instance through the bad Planner for the first time" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "PBS sends an alert request to the Alert Service for the first time" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } + + when: "Initiating PBS to register its instance through the bad Planner until the period threshold of alerts is reached" + (2..DEFAULT_ALERT_PERIOD).forEach { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + } + + then: "PBS sends an alert request to the Alert Service for the second time" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 2 } + + and: "Request has the right number of failed register attempts" + def alertRequest = alert.recordedAlertRequest + assert alertRequest.details.startsWith("Service register failed to send request $DEFAULT_ALERT_PERIOD " + + "time(s) with error message") + + cleanup: "Return initial Planner response status code" + generalPlanner.initRegisterResponse() + } + + def "PBS should send an alert request with appropriate headers"() { + given: "Changed Planner Register endpoint response to return bad status code" + generalPlanner.initRegisterResponse(NOT_FOUND_404) + + and: "PBS alert counter is reset" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) + + and: "Initial Alert Service request count is taken" + def initialRequestCount = alert.requestCount + + when: "Initiating PBS to register its instance through the bad Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + and: "PBS sends an alert request to the Alert Service" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } + + then: "Request headers correspond to the payload" + def alertRequestHeaders = alert.lastRecordedAlertRequestHeaders + assert alertRequestHeaders + + and: "Request has an authorization header with a basic auth token" + def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) + assert alertRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] + + and: "Request has a header with uuid value" + def uuidHeaderValue = alertRequestHeaders.get(PG_TRX_ID_HEADER) + assert uuidHeaderValue?.size() == 1 + assert (uuidHeaderValue[0] =~ UUID_REGEX).matches() + + and: "Request has a content type header" + assert alertRequestHeaders.get(CONTENT_TYPE_HEADER) == [CONTENT_TYPE_HEADER_VALUE] + + cleanup: "Return initial Planner response status code" + generalPlanner.initRegisterResponse() + } + + @Unroll + def "PBS should send an alert when fetching line items response status wasn't OK ('#httpStatusCode')"() { + given: "Changed Planner line items endpoint response to return bad status code" + // PBS will make 2 requests to the planner: 1 normal, 2 - recovery request + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString), httpStatusCode, Times.exactly(2)) + + and: "PBS alert counter is reset" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) + + and: "Initial Alert Service request count is taken" + def initialRequestCount = alert.requestCount + + when: "Initiating PBS to fetch line items through the bad Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + then: "PBS sends an alert request to the Alert Service" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } + + and: "Alert request should correspond to the payload" + verifyAll(alert.recordedAlertRequest) { alertRequest -> + (alertRequest.id =~ UUID_REGEX).matches() + alertRequest.action == RAISE + alertRequest.priority == MEDIUM + alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) + alertRequest.name == PBS_PLANNER_CLIENT_ERROR + alertRequest.details == "Service planner failed to send request 1 time(s) with error message :" + + " Failed to retrieve line items from GP. Reason: Failed to fetch data from Planner, HTTP status code ${httpStatusCode.code()}" + + alertRequest.source.env == pgConfig.env + alertRequest.source.dataCenter == pgConfig.dataCenter + alertRequest.source.region == pgConfig.region + alertRequest.source.system == pgConfig.system + alertRequest.source.subSystem == pgConfig.subSystem + alertRequest.source.hostId == pgConfig.hostId + } + + cleanup: "Return initial Planner response status code" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) + + where: "Bad status codes" + httpStatusCode << [NO_CONTENT_204, NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] + } + + @Unroll + def "PBS should send an alert when register PBS instance response status wasn't OK ('#httpStatusCode')"() { + given: "Changed Planner register endpoint response to return bad status code" + generalPlanner.initRegisterResponse(httpStatusCode) + + and: "PBS alert counter is reset" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) + + and: "Initial Alert Service request count is taken" + def initialRequestCount = alert.requestCount + + when: "Initiating PBS to register its instance through the bad Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "PBS sends an alert request to the Alert Service" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } + + and: "Alert request should correspond to the payload" + verifyAll(alert.recordedAlertRequest) { alertRequest -> + (alertRequest.id =~ UUID_REGEX).matches() + alertRequest.action == RAISE + alertRequest.priority == MEDIUM + alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) + alertRequest.name == PBS_REGISTER_CLIENT_ERROR + alertRequest.details.startsWith("Service register failed to send request 1 time(s) with error message :" + + " Planner responded with non-successful code ${httpStatusCode.code()}") + + alertRequest.source.env == pgConfig.env + alertRequest.source.dataCenter == pgConfig.dataCenter + alertRequest.source.region == pgConfig.region + alertRequest.source.system == pgConfig.system + alertRequest.source.subSystem == pgConfig.subSystem + alertRequest.source.hostId == pgConfig.hostId + } + + cleanup: "Return initial Planner response status code" + generalPlanner.initRegisterResponse() + + where: "Bad status codes" + httpStatusCode << [NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] + } + + @Unroll + def "PBS should send an alert when send delivery statistics report response status wasn't OK ('#httpStatusCode')"() { + given: "Changed Delivery Statistics endpoint response to return bad status code" + deliveryStatistics.reset() + deliveryStatistics.setResponse(httpStatusCode) + + and: "Set line items response" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) + + and: "PBS alert counter is reset" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) + + and: "Initial Alert Service request count is taken" + def initialRequestCount = alert.requestCount + + and: "Report to send is generated by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + when: "Initiating PBS to send delivery statistics report through the bad Delivery Statistics Service" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends an alert request to the Alert Service" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } + + and: "Alert request should correspond to the payload" + verifyAll(alert.recordedAlertRequest) { alertRequest -> + (alertRequest.id =~ UUID_REGEX).matches() + alertRequest.action == RAISE + alertRequest.priority == MEDIUM + alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) + alertRequest.name == PBS_DELIVERY_CLIENT_ERROR + alertRequest.details.startsWith("Service deliveryStats failed to send request 1 time(s) with error message : " + + "Report was not send to delivery stats service with a reason: Delivery stats service responded with " + + "status code = ${httpStatusCode.code()} for report with id = ") + + alertRequest.source.env == pgConfig.env + alertRequest.source.dataCenter == pgConfig.dataCenter + alertRequest.source.region == pgConfig.region + alertRequest.source.system == pgConfig.system + alertRequest.source.subSystem == pgConfig.subSystem + alertRequest.source.hostId == pgConfig.hostId + } + + cleanup: "Return initial Delivery Statistics response status code" + deliveryStatistics.reset() + deliveryStatistics.setResponse() + + where: "Bad status codes" + httpStatusCode << [NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] + } + + def "PBS should send an alert when Planner returns empty response"() { + given: "Changed Planner get plans response to return no plans" + generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) + + and: "PBS alert counter is reset" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.resetAlertCountRequest) + + and: "Initial Alert Service request count is taken" + def initialRequestCount = alert.requestCount + + when: "Initiating PBS to fetch line items through the Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + then: "PBS sends an alert request to the Alert Service" + PBSUtils.waitUntil { alert.requestCount == initialRequestCount + 1 } + + and: "Alert request should correspond to the payload" + verifyAll(alert.recordedAlertRequest) { alertRequest -> + (alertRequest.id =~ UUID_REGEX).matches() + alertRequest.action == RAISE + alertRequest.priority == LOW + alertRequest.updatedAt.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) + alertRequest.name == PBS_PLANNER_EMPTY_RESPONSE + alertRequest.details.startsWith("Service planner failed to send request 1 time(s) with error message : " + + "Response without line items was received from planner") + + alertRequest.source.env == pgConfig.env + alertRequest.source.dataCenter == pgConfig.dataCenter + alertRequest.source.region == pgConfig.region + alertRequest.source.system == pgConfig.system + alertRequest.source.subSystem == pgConfig.subSystem + alertRequest.source.hostId == pgConfig.hostId + } + + cleanup: "Return initial Planner response" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/BasePgSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/BasePgSpec.groovy new file mode 100644 index 00000000000..6d27a8e20bb --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/BasePgSpec.groovy @@ -0,0 +1,57 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.Dependencies +import org.prebid.server.functional.testcontainers.PBSTest +import org.prebid.server.functional.testcontainers.PbsPgConfig +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.testcontainers.scaffolding.Bidder +import org.prebid.server.functional.testcontainers.scaffolding.pg.Alert +import org.prebid.server.functional.testcontainers.scaffolding.pg.DeliveryStatistics +import org.prebid.server.functional.testcontainers.scaffolding.pg.GeneralPlanner +import org.prebid.server.functional.testcontainers.scaffolding.pg.UserData +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Specification + +@PBSTest +// TODO migrate this to extend BaseSpec +abstract class BasePgSpec extends Specification { + + protected static final ObjectMapperWrapper mapper = Dependencies.objectMapperWrapper + protected static final PbsServiceFactory pbsServiceFactory = new PbsServiceFactory(Dependencies.networkServiceContainer, mapper) + + protected static final GeneralPlanner generalPlanner = new GeneralPlanner(Dependencies.networkServiceContainer, mapper) + protected static final DeliveryStatistics deliveryStatistics = new DeliveryStatistics(Dependencies.networkServiceContainer, mapper) + protected static final Alert alert = new Alert(Dependencies.networkServiceContainer, mapper) + protected static final UserData userData = new UserData(Dependencies.networkServiceContainer, mapper) + + protected static final PbsPgConfig pgConfig = new PbsPgConfig(Dependencies.networkServiceContainer) + protected static final PrebidServerService pgPbsService = pbsServiceFactory.getService(pgConfig.properties) + protected static final Bidder bidder = new Bidder(Dependencies.networkServiceContainer, mapper) + + def setupSpec() { + generalPlanner.setResponse() + + deliveryStatistics.setResponse() + + alert.setResponse() + + userData.setResponse() + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) + } + + def cleanupSpec() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + } + + protected static void updateLineItemsAndWait() { + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/CurrencySpec.groovy new file mode 100644 index 00000000000..7c6216b496f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/CurrencySpec.groovy @@ -0,0 +1,93 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.deals.lineitem.LineItem +import org.prebid.server.functional.model.deals.lineitem.Price +import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import spock.lang.Shared + +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer + +class CurrencySpec extends BasePgSpec { + + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer, mapper).tap { + setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.defaultCurrencyConversionRatesResponse) + } + + private static final Map pgCurrencyConverterPbsConfig = externalCurrencyConverterConfig + pgConfig.properties + private static final PrebidServerService pgCurrencyConverterPbsService = pbsServiceFactory.getService(pgCurrencyConverterPbsConfig) + + @Shared + BidRequest bidRequest + + def setup() { + bidRequest = BidRequest.defaultBidRequest + bidder.setResponse(bidRequest.id, BidResponse.getDefaultBidResponse(bidRequest)) + pgCurrencyConverterPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should convert non-default line item currency to the default one during the bidder auction"() { + given: "Planner Mock line items with the same CPM but different currencies" + def accountId = bidRequest.site.publisher.id + def defaultCurrency = Price.defaultPrice.currency + def nonDefaultCurrency = "EUR" + def defaultCurrencyLineItem = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: defaultCurrency) }] + def nonDefaultCurrencyLineItems = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: nonDefaultCurrency) }, + LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: nonDefaultCurrency) }, + LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: nonDefaultCurrency) }] + def lineItems = defaultCurrencyLineItem + nonDefaultCurrencyLineItems + def plansResponse = new PlansResponse(lineItems: lineItems) + generalPlanner.initPlansResponse(plansResponse) + def nonDefaultCurrencyLineItemIds = nonDefaultCurrencyLineItems.collect { it.lineItemId } + + and: "Line items are fetched by PBS" + pgCurrencyConverterPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is requested" + def auctionResponse = pgCurrencyConverterPbsService.sendAuctionRequest(bidRequest) + + then: "All line items are ready to be served" + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == plansResponse.lineItems.size() + + and: "Line Item with EUR defaultCurrency was sent to bidder as EUR defaultCurrency rate > than USD" + assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value)?.sort() == + nonDefaultCurrencyLineItemIds.sort() + } + + def "PBS should invalidate line item with an unknown for the conversion rate currency"() { + given: "Planner Mock line items with a default currency and unknown currency" + def defaultCurrency = Price.defaultPrice.currency + def unknownCurrency = "UAH" + def defaultCurrencyLineItem = [LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { price = new Price(cpm: 1, currency: defaultCurrency) }] + def unknownCurrencyLineItem = [LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { price = new Price(cpm: 1, currency: unknownCurrency) }] + def lineItems = defaultCurrencyLineItem + unknownCurrencyLineItem + def plansResponse = new PlansResponse(lineItems: lineItems) + generalPlanner.initPlansResponse(plansResponse) + def defaultCurrencyLineItemId = defaultCurrencyLineItem.collect { it.lineItemId } + + and: "Line items are fetched by PBS" + pgCurrencyConverterPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is requested" + def auctionResponse = pgCurrencyConverterPbsService.sendAuctionRequest(bidRequest) + + then: "Only line item with the default currency is ready to be served and was sent to bidder" + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe == defaultCurrencyLineItemId as Set + assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) == + defaultCurrencyLineItemId as Set + } + + private static Map getExternalCurrencyConverterConfig() { + ["currency-converter.external-rates.enabled" : "true", + "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), + "currency-converter.external-rates.default-timeout-ms": "4000", + "currency-converter.external-rates.refresh-period-ms" : "900000" + ] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgAuctionSpec.groovy new file mode 100644 index 00000000000..c3762ef5255 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/PgAuctionSpec.groovy @@ -0,0 +1,537 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.deals.lineitem.FrequencyCap +import org.prebid.server.functional.model.deals.lineitem.LineItem +import org.prebid.server.functional.model.deals.lineitem.LineItemSize +import org.prebid.server.functional.model.deals.lineitem.Price +import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting +import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.BidRequestExt +import org.prebid.server.functional.model.request.auction.Bidder +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Prebid +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Unroll + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.deals.lineitem.LineItemStatus.DELETED +import static org.prebid.server.functional.model.deals.lineitem.LineItemStatus.PAUSED +import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.HIGH +import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.LOW +import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.MEDIUM +import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.VERY_HIGH +import static org.prebid.server.functional.model.deals.lineitem.RelativePriority.VERY_LOW +import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.IN +import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.INTERSECTS +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_MEDIA_TYPE +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_SIZE +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.DEVICE_REGION +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX + +class PgAuctionSpec extends BasePgSpec { + + def cleanup() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should return base response after PG auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Auction response contains values according to the payload" + verifyAll(auctionResponse) { + auctionResponse.id == bidRequest.id + auctionResponse.cur == pgConfig.currency + !auctionResponse.bidid + !auctionResponse.customdata + !auctionResponse.nbr + } + + and: "Seat bid corresponds to the request seat bid" + assert auctionResponse.seatbid?.size() == bidRequest.imp.size() + def seatBid = auctionResponse.seatbid[0] + assert seatBid.seat == GENERIC.value + + assert seatBid.bid?.size() == 1 + + verifyAll(seatBid.bid[0]) { bid -> + (bid.id =~ UUID_REGEX).matches() + bid.impid == bidRequest.imp[0].id + bid.price == bidResponse.seatbid[0].bid[0].price + bid.crid == bidResponse.seatbid[0].bid[0].crid + bid.ext?.prebid?.type == BANNER + bid.ext?.origbidcpm == bidResponse.seatbid[0].bid[0].price + } + } + + @Unroll + def "PBS shouldn't process line item with #reason"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock non matched line item" + generalPlanner.initPlansResponse(plansResponse.tap { + it.lineItems[0].accountId = bidRequest.site.publisher.id + }) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start processing PG deals" + assert !auctionResponse.ext?.debug?.pgmetrics + + where: + reason | plansResponse + + "non matched targeting" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { + lineItems[0].targeting = new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) + .addTargeting(DEVICE_REGION, IN, [14]) + .build() + } + + "empty targeting" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { + lineItems[0].targeting = null + } + + "non matched bidder" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { + lineItems[0].source = PBSUtils.randomString + } + + "inactive status" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { + lineItems[0].status = DELETED + } + + "paused status" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { + lineItems[0].status = PAUSED + } + + "expired lifetime" | PlansResponse.getDefaultPlansResponse(PBSUtils.randomString).tap { + lineItems[0].startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusMinutes(2) + lineItems[0].endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusMinutes(1) + lineItems[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusMinutes(2) + } + } + + def "PBS shouldn't process line item with non matched publisher account id"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock non matched publisher account id line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(PBSUtils.randomNumber as String) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start processing PG deals" + assert !auctionResponse.ext?.debug?.pgmetrics + } + + def "PBS shouldn't start processing PG deals when there is no any line item"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock no line items" + generalPlanner.initPlansResponse(new PlansResponse(lineItems: [])) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start processing PG deals" + assert !auctionResponse.ext?.debug?.pgmetrics + } + + @Unroll + def "PBS shouldn't allow line item with #reason delivery plan take part in auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line item with expired delivery schedule" + def plansResponse = plansResponseClosure(bidRequest) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't allow line item take part in auction" + assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == + plansResponse.lineItems.collect { it.lineItemId } as Set + + where: + reason | plansResponseClosure + + "expired" | { BidRequest bidReq -> + PlansResponse.getDefaultPlansResponse(bidReq.site.publisher.id).tap { + lineItems[0].deliverySchedules[0].startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(2) + lineItems[0].deliverySchedules[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(2) + lineItems[0].deliverySchedules[0].endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).minusDays(1) + } + } + + "not set" | { BidRequest bidReq -> + PlansResponse.getDefaultPlansResponse(bidReq.site.publisher.id).tap { + lineItems[0].deliverySchedules = [] + } + } + } + + def "PBS should process only first #maxDealsPerBidder line items among the matched ones"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Publisher account id" + def accountId = bidRequest.site.publisher.id + + and: "Planner Mock line items to return #maxDealsPerBidder + 1 line items" + def maxLineItemsToProcess = pgConfig.maxDealsPerBidder + def plansResponse = new PlansResponse(lineItems: (1..maxLineItemsToProcess + 1).collect { + LineItem.getDefaultLineItem(accountId) + }) + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "There are #maxLineItemsToProcess + 1 line items are matched and ready to serve" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == maxLineItemsToProcess + 1 + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == maxLineItemsToProcess + 1 + + and: "Only #maxLineItemsToProcess were sent to the bidder" + assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value)?.size() == maxLineItemsToProcess + } + + def "PBS should send to bidder only the first line item among line items with identical deal ids"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Deal id" + def dealId = PBSUtils.randomString + + and: "Planner Mock line items with identical deal ids" + def plansResponse = new PlansResponse(lineItems: (1..2).collect { + LineItem.getDefaultLineItem(bidRequest.site.publisher.id).tap { it.dealId = dealId } + }) + generalPlanner.initPlansResponse(plansResponse) + def lineItemsNumber = plansResponse.lineItems.size() + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "There are 2 matched and ready to serve line items" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemsNumber + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItemsNumber + + and: "Only 1 line item was sent to the bidder" + assert auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value)?.size() == lineItemsNumber - 1 + } + + def "PBS should allow line item with matched to the request bidder alias take part in auction"() { + given: "Bid request with set bidder alias" + def lineItemSource = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + def prebid = new Prebid(aliases: [(lineItemSource): GENERIC.value], debug: 1) + ext = new BidRequestExt(prebid: prebid) + } + + and: "Planner Mock line items with changed line item source" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].source = lineItemSource + } + generalPlanner.initPlansResponse(plansResponse) + def lineItemCount = plansResponse.lineItems.size() + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Line item was matched by alias bidder and took part in auction" + def pgMetrics = auctionResponse.ext?.debug?.pgmetrics + assert pgMetrics + assert pgMetrics.sentToBidder?.get(lineItemSource)?.size() == lineItemCount + assert pgMetrics.readyToServe?.size() == lineItemCount + assert pgMetrics.matchedWholeTargeting?.size() == lineItemCount + } + + def "PBS should abandon line items with matched user frequency capped ids take part in auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items with added frequency cap" + def fcapId = PBSUtils.randomNumber as String + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].frequencyCaps = [FrequencyCap.defaultFrequencyCap.tap { it.fcapId = fcapId }] + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + and: "User Service Response is set to return frequency capped id identical to the line item fcapId" + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse.tap { + user.ext.fcapIds = [fcapId] + }) + + and: "Cookies header" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS hasn't started processing PG deals as line item was recognized as frequency capped" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedTargetingFcapped?.size() == plansResponse.lineItems.size() + + cleanup: + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) + } + + def "PBS should allow line items with unmatched user frequency capped ids take part in auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items with added frequency cap" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].frequencyCaps = [FrequencyCap.defaultFrequencyCap.tap { fcapId = PBSUtils.randomNumber as String }] + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + and: "User Service Response is set to return frequency capped id not identical to the line item fcapId" + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse.tap { + user.ext.fcapIds = [PBSUtils.randomNumber as String] + }) + + and: "Cookies header" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS hasn't started processing PG deals as line item was recognized as frequency capped" + assert !auctionResponse.ext?.debug?.pgmetrics?.matchedTargetingFcapped + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe + + cleanup: + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) + } + + def "PBS shouldn't use already matched line items by the same bidder during one auction"() { + given: "Bid request with two impressions" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder = new Bidder(generic: new Generic()) + imp << Imp.defaultImpression + imp[1].ext.prebid.bidder = new Bidder(generic: new Generic()) + } + def accountId = bidRequest.site.publisher.id + + and: "Planner Mock with two line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(accountId).tap { + lineItems << LineItem.getDefaultLineItem(accountId) + } + generalPlanner.initPlansResponse(plansResponse) + def lineItemCount = plansResponse.lineItems.size() + def lineItemIds = plansResponse.lineItems.collect { it.lineItemId } as Set + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is requested" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Two line items are ready to be served" + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItemCount + + and: "Two (as the number of imps) different line items were sent to the bidder" + def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == lineItemCount + assert sentToBidder.sort() == lineItemIds.sort() + + def sentToBidderAsTopMatch = auctionResponse.ext?.debug?.pgmetrics?.sentToBidderAsTopMatch?.get(GENERIC.value) + assert sentToBidderAsTopMatch?.size() == lineItemCount + assert sentToBidderAsTopMatch.sort() == lineItemIds.sort() + } + + def "PBS should send line items with the highest priority to the bidder during auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + def accountId = bidRequest.site.publisher.id + + and: "Planner Mock line items with different priorities" + def lowerPriorityLineItems = [LineItem.getDefaultLineItem(accountId).tap { relativePriority = VERY_LOW }, + LineItem.getDefaultLineItem(accountId).tap { relativePriority = LOW }] + def higherPriorityLineItems = [LineItem.getDefaultLineItem(accountId).tap { relativePriority = MEDIUM }, + LineItem.getDefaultLineItem(accountId).tap { relativePriority = HIGH }, + LineItem.getDefaultLineItem(accountId).tap { relativePriority = VERY_HIGH }] + def lineItems = lowerPriorityLineItems + higherPriorityLineItems + def plansResponse = new PlansResponse(lineItems: lineItems) + def higherPriorityLineItemIds = higherPriorityLineItems.collect { it.lineItemId } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is requested" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "All line items are ready to be served" + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItems.size() + + and: "#maxDealsPerBidder[3] line items were send to bidder" + def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == pgConfig.maxDealsPerBidder + + and: "Those line items with the highest priority were sent" + assert sentToBidder.sort() == higherPriorityLineItemIds.sort() + } + + def "PBS should send line items with the highest CPM to the bidder during auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + def accountId = bidRequest.site.publisher.id + + and: "Planner Mock line items with different CPMs" + def currency = Price.defaultPrice.currency + def lowerCpmLineItems = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 1, currency: currency) }, + LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 2, currency: currency) }] + def higherCpmLineItems = [LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 3, currency: currency) }, + LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 4, currency: currency) }, + LineItem.getDefaultLineItem(accountId).tap { price = new Price(cpm: 5, currency: currency) }] + def lineItems = lowerCpmLineItems + higherCpmLineItems + def plansResponse = new PlansResponse(lineItems: lineItems) + def higherCpmLineItemIds = higherCpmLineItems.collect { it.lineItemId } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is requested" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "All line items are ready to be served" + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItems.size() + + and: "#maxDealsPerBidder[3] line items were send to bidder" + def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == pgConfig.maxDealsPerBidder + + and: "Those line items with the highest CPM were sent" + assert sentToBidder.sort() == higherCpmLineItemIds.sort() + } + + def "PBS should send line items with the highest priority to the bidder during auction despite the price"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + def accountId = bidRequest.site.publisher.id + + and: "Planner Mock line items with different priorities and CPMs" + def currency = Price.defaultPrice.currency + def lowPriorityHighPriceLineItems = + [LineItem.getDefaultLineItem(accountId).tap { + relativePriority = VERY_LOW + price = new Price(cpm: 5, currency: currency) + }, + LineItem.getDefaultLineItem(accountId).tap { + relativePriority = LOW + price = new Price(cpm: 5, currency: currency) + }] + def highPriorityLowPriceLineItems = + [LineItem.getDefaultLineItem(accountId).tap { + relativePriority = MEDIUM + price = new Price(cpm: 1, currency: currency) + }, + LineItem.getDefaultLineItem(accountId).tap { + relativePriority = HIGH + price = new Price(cpm: 1, currency: currency) + }, + LineItem.getDefaultLineItem(accountId).tap { + relativePriority = VERY_HIGH + price = new Price(cpm: 1, currency: currency) + }] + def lineItems = lowPriorityHighPriceLineItems + highPriorityLowPriceLineItems + def plansResponse = new PlansResponse(lineItems: lineItems) + generalPlanner.initPlansResponse(plansResponse) + def higherPriorityLineItemIds = highPriorityLowPriceLineItems.collect { it.lineItemId } + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "All line items are ready to be served" + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItems.size() + + and: "#maxDealsPerBidder[3] line items were send to bidder" + def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == pgConfig.maxDealsPerBidder + + and: "Those line items with the highest priority were sent" + assert sentToBidder.sort() == higherPriorityLineItemIds.sort() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidResponseSpec.groovy new file mode 100644 index 00000000000..1664e118351 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidResponseSpec.groovy @@ -0,0 +1,166 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.deals.lineitem.LineItemSize +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Format +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC + +class PgBidResponseSpec extends BasePgSpec { + + def cleanup() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should allow valid bidder response with deals info continue taking part in auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + def lineItemCount = plansResponse.lineItems.size() + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Set bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder returned valid response with deals info during auction" + assert auctionResponse.ext?.debug?.pgmetrics?.sentToClient?.size() == lineItemCount + assert auctionResponse.ext?.debug?.pgmetrics?.sentToClientAsTopMatch?.size() == lineItemCount + } + + def "PBS should invalidate bidder response when bid id doesn't match to the bid request bid id"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Set bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { + seatbid[0].bid[0].impid = PBSUtils.randomNumber as String + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder response is invalid" + def bidderError = auctionResponse.ext?.errors?.get(GENERIC) + assert bidderError?.size() == 1 + assert bidderError[0].message.startsWith("Bid \"${bidResponse.seatbid[0].bid[0].id}\" has no corresponding imp in request") + } + + def "PBS should invalidate bidder response when deal id doesn't match to the bid request deal id"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Set bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { + seatbid[0].bid[0].dealid = PBSUtils.randomNumber as String + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder response is invalid" + def bidderError = auctionResponse.ext?.errors?.get(GENERIC) + assert bidderError?.size() == 1 + assert bidderError[0].message.startsWith("WARNING: Bid \"${bidResponse.seatbid[0].bid[0].id}\" has 'dealid' not present in corresponding imp in request.") + } + + def "PBS should invalidate bidder response when non-matched to the bid request size is returned"() { + given: "Bid request with set sizes" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [Format.defaultFormat] + } + def impFormat = bidRequest.imp[0].banner.format[0] + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Set bid response with unmatched to the bid request size" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { + seatbid[0].bid[0].w = PBSUtils.randomNumber + } + def bid = bidResponse.seatbid[0].bid[0] + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder response is invalid" + assert auctionResponse.ext?.debug?.pgmetrics?.responseInvalidated?.size() == plansResponse.lineItems.size() + + and: "PBS invalidated response as unmatched by size" + def bidderError = auctionResponse.ext?.errors?.get(GENERIC) + assert bidderError?.size() == 1 + assert bidderError[0].message == "Bid \"$bid.id\" has 'w' and 'h' not supported by " + + "corresponding imp in request. Bid dimensions: '${bid.w}x$bid.h', formats in imp: '${impFormat.w}x$impFormat.h'" + } + + def "PBS should invalidate bidder response when non-matched to the PBS line item size response is returned"() { + given: "Bid request" + def newFormat = new Format(w: PBSUtils.randomNumber, h: PBSUtils.randomNumber) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [Format.defaultFormat, newFormat] + } + + and: "Planner Mock line items with a default size" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].sizes = [LineItemSize.defaultLineItemSize] + } + def lineItemSize = plansResponse.lineItems[0].sizes[0] + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Set bid response with non-matched to the line item size" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse).tap { + seatbid[0].bid[0].w = newFormat.w + seatbid[0].bid[0].h = newFormat.h + } + def bid = bidResponse.seatbid[0].bid[0] + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder response is invalid" + assert auctionResponse.ext?.debug?.pgmetrics?.responseInvalidated?.size() == plansResponse.lineItems.size() + + and: "PBS invalidated response as not matched by size" + def bidderError = auctionResponse.ext?.errors?.get(GENERIC) + assert bidderError?.size() == 1 + assert bidderError[0].message == "Bid \"$bid.id\" has 'w' and 'h' not matched to Line Item. " + + "Bid dimensions: '${bid.w}x$bid.h', Line Item sizes: '${lineItemSize.w}x$lineItemSize.h'" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidderRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidderRequestSpec.groovy new file mode 100644 index 00000000000..9f5bca6200c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/PgBidderRequestSpec.groovy @@ -0,0 +1,167 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.deals.lineitem.LineItem +import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Bidder +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Ignore + +class PgBidderRequestSpec extends BasePgSpec { + + def cleanup() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should be able to add given device info to the bidder request"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + device = new Device(ua: PBSUtils.randomString, + make: PBSUtils.randomString, + model: PBSUtils.randomString) + } + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "User Service response is set" + def userResponse = UserDetailsResponse.defaultUserResponse + userData.setUserDataResponse(userResponse) + + and: "Cookies with user ids" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS sent a request to the bidder with added device info" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { bidderRequest -> + bidderRequest.user?.ext?.fcapids == userResponse.user.ext.fcapIds + bidderRequest.user.data?.size() == userResponse.user.data.size() + bidderRequest.user.data[0].id == userResponse.user.data[0].name + bidderRequest.user.data[0].segment?.size() == userResponse.user.data[0].segment.size() + bidderRequest.user.data[0].segment[0].id == userResponse.user.data[0].segment[0].id + } + } + + def "PBS should be able to add pmp deals part to the bidder request when PG is enabled"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def accountId = bidRequest.site.publisher.id + def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(accountId), LineItem.getDefaultLineItem(accountId)]) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Sending auction request to PBS" + pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS sent a request to the bidder with added deals" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + assert bidderRequest.imp?.size() == bidRequest.imp.size() + assert bidderRequest.imp[0].pmp?.deals?.size() == plansResponse.lineItems.size() + assert bidderRequest.imp[0].pmp?.deals + assert plansResponse.lineItems.each { lineItem -> + def deal = bidderRequest.imp[0]?.pmp?.deals?.find { it.id == lineItem.dealId } + + assert deal + verifyAll(deal) { + deal?.ext?.line?.lineItemId == lineItem.lineItemId + deal?.ext?.line?.extLineItemId == lineItem.extLineItemId + deal?.ext?.line?.sizes?.size() == lineItem.sizes.size() + deal?.ext?.line?.sizes[0].w == lineItem.sizes[0].w + deal?.ext?.line?.sizes[0].h == lineItem.sizes[0].h + } + } + } + + @Ignore(value = "https://jira.magnite-core.com/browse/HB-13821") + def "PBS shouldn't add already top matched line item by first impression to the second impression deals bidder request section"() { + given: "Bid request with two impressions" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder = new Bidder(generic: new Generic()) + imp << Imp.defaultImpression + imp[1].ext.prebid.bidder = new Bidder(generic: new Generic()) + } + + and: "Planner Mock line items" + def accountId = bidRequest.site.publisher.id + def plansResponse = new PlansResponse(lineItems: [LineItem.getDefaultLineItem(accountId), LineItem.getDefaultLineItem(accountId)]) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Sending auction request to PBS" + pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS sent a request to the bidder with two impressions" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp?.size() == bidRequest.imp.size() + + and: "First impression contains 2 deals according to the line items" + def firstRequestImp = bidderRequest.imp.find { it.id == bidRequest.imp[0].id } + assert firstRequestImp?.pmp?.deals?.size() == plansResponse.lineItems.size() + assert plansResponse.lineItems.each { lineItem -> + def deal = firstRequestImp.pmp.deals.find { it.id == lineItem.dealId } + + assert deal + verifyAll(deal) { + deal?.ext?.line?.lineItemId == lineItem.lineItemId + deal?.ext?.line?.extLineItemId == lineItem.extLineItemId + deal?.ext?.line?.sizes?.size() == lineItem.sizes.size() + deal?.ext?.line?.sizes[0].w == lineItem.sizes[0].w + deal?.ext?.line?.sizes[0].h == lineItem.sizes[0].h + } + } + + def topMatchLineItemId = firstRequestImp.pmp.deals.first().ext.line.lineItemId + def secondRequestImp = bidderRequest.imp.find { it.id == bidRequest.imp[1].id } + + and: "Second impression contains only 1 deal excluding already top matched line items by the first impression" + assert secondRequestImp.pmp.deals.size() == plansResponse.lineItems.size() - 1 + assert !(secondRequestImp.pmp.deals.collect { it.ext.line.lineItemId } in topMatchLineItemId) + + assert plansResponse.lineItems.findAll { it.lineItemId != topMatchLineItemId }.each { lineItem -> + def deal = secondRequestImp.pmp.deals.find { it.id == lineItem.dealId } + + assert deal + verifyAll(deal) { + deal?.ext?.line?.lineItemId == lineItem.lineItemId + deal?.ext?.line?.extLineItemId == lineItem.extLineItemId + deal?.ext?.line?.sizes?.size() == lineItem.sizes.size() + deal?.ext?.line?.sizes[0].w == lineItem.sizes[0].w + deal?.ext?.line?.sizes[0].h == lineItem.sizes[0].h + } + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/PlansSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/PlansSpec.groovy new file mode 100644 index 00000000000..70b9b95dd21 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/PlansSpec.groovy @@ -0,0 +1,63 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils + +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 +import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER +import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER +import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX + +class PlansSpec extends BasePgSpec { + + def "PBS should be able to send a request to General Planner"() { + given: "Initial request count" + def initialRequestCount = generalPlanner.recordedPlansRequestCount + + and: "General Planner response is set" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString), OK_200) + + when: "PBS sends request to General Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + then: "Request is sent" + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialRequestCount + 1 } + } + + def "PBS should retry request to General Planner when first request fails"() { + given: "Initial request count" + def initialRequestCount = generalPlanner.recordedPlansRequestCount + + and: "Bad General Planner response is set" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString), INTERNAL_SERVER_ERROR_500) + + when: "PBS sends request to General Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + then: "Request is sent two times" + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialRequestCount + 2 } + } + + def "PBS should send appropriate headers when requests plans from General Planner"() { + when: "PBS sends request to General Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + then: "Request with headers is sent" + def plansRequestHeaders = generalPlanner.lastRecordedPlansRequestHeaders + assert plansRequestHeaders + + and: "Request has an authorization header with a basic auth token" + def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) + assert plansRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] + + and: "Request has a header with uuid value" + def uuidHeader = plansRequestHeaders.get(PG_TRX_ID_HEADER) + assert uuidHeader?.size() == 1 + assert (uuidHeader[0] =~ UUID_REGEX).matches() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/RegisterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/RegisterSpec.groovy new file mode 100644 index 00000000000..d3be6b6ba25 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/RegisterSpec.groovy @@ -0,0 +1,229 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER +import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER +import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX + +class RegisterSpec extends BasePgSpec { + + def setupSpec() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should be able to register its instance in Planner on demand"() { + given: "Properties values from PBS config" + def host = pgConfig.hostId + def vendor = pgConfig.vendor + def region = pgConfig.region + + and: "Initial Planner request count" + def initialRequestCount = generalPlanner.requestCount + + when: "PBS sends request to Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "Request counter is increased" + PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } + + and: "PBS instance is healthy" + def registerRequest = generalPlanner.lastRecordedRegisterRequest + assert registerRequest.healthIndex >= 0 && registerRequest.healthIndex <= 1 + + and: "Host, vendor and region are appropriate to the config" + assert registerRequest.hostInstanceId == host + assert registerRequest.vendor == vendor + assert registerRequest.region == region + + and: "Delivery Statistics Report doesn't have delivery specific data" + verifyAll(registerRequest.status.dealsStatus) { delStatsReport -> + (delStatsReport.reportId =~ UUID_REGEX).matches() + delStatsReport.instanceId == host + delStatsReport.vendor == vendor + delStatsReport.region == region + !delStatsReport.lineItemStatus + !delStatsReport.dataWindowStartTimeStamp + !delStatsReport.dataWindowEndTimeStamp + delStatsReport.reportTimeStamp.isBefore(ZonedDateTime.now(ZoneId.from(UTC))) + } + } + + def "PBS should send a register request with appropriate headers"() { + when: "Initiating PBS to register its instance" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "Request with headers is sent" + def registerRequestHeaders = generalPlanner.lastRecordedRegisterRequestHeaders + assert registerRequestHeaders + + and: "Request has an authorization header with a basic auth token" + def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) + assert registerRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] + + and: "Request has a header with uuid value" + def uuidHeader = registerRequestHeaders.get(PG_TRX_ID_HEADER) + assert uuidHeader?.size() == 1 + assert (uuidHeader[0] =~ UUID_REGEX).matches() + } + + def "PBS should be able to register its instance in Planner providing active PBS line items info"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + def lineItem = plansResponse.lineItems[0] + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial Planner request count" + def initialRequestCount = generalPlanner.requestCount + + when: "PBS sends request to Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "Request counter is increased" + PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } + + and: "Delivery Statistics Report has active line item data" + def registerRequest = generalPlanner.lastRecordedRegisterRequest + def delStatsReport = registerRequest.status?.dealsStatus + assert delStatsReport + def lineItemStatus = delStatsReport.lineItemStatus + + assert lineItemStatus?.size() == plansResponse.lineItems.size() + verifyAll(lineItemStatus) { + lineItemStatus[0].lineItemSource == lineItem.source + lineItemStatus[0].lineItemId == lineItem.lineItemId + lineItemStatus[0].dealId == lineItem.dealId + lineItemStatus[0].extLineItemId == lineItem.extLineItemId + } + + and: "Line item wasn't used in auction" + verifyAll(lineItemStatus) { + !lineItemStatus[0].accountAuctions + !lineItemStatus[0].targetMatched + !lineItemStatus[0].sentToBidder + !lineItemStatus[0].spentTokens + } + + cleanup: + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should be able to register its instance in Planner providing line items status after auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + def lineItem = plansResponse.lineItems[0] + def lineItemCount = plansResponse.lineItems.size() as Long + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial Planner request count" + def initialRequestCount = generalPlanner.requestCount + + and: "Auction is requested" + pgPbsService.sendAuctionRequest(bidRequest) + + when: "PBS sends request to Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "Request counter is increased" + PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } + + and: "Delivery Statistics Report has info about auction" + def registerRequest = generalPlanner.lastRecordedRegisterRequest + def delStatsReport = registerRequest.status.dealsStatus + assert delStatsReport + + and: "Delivery Statistics Report has correct line item status data" + def lineItemStatus = delStatsReport.lineItemStatus + + assert lineItemStatus?.size() as Long == lineItemCount + verifyAll(lineItemStatus) { + lineItemStatus[0].lineItemSource == lineItem.source + lineItemStatus[0].lineItemId == lineItem.lineItemId + lineItemStatus[0].dealId == lineItem.dealId + lineItemStatus[0].extLineItemId == lineItem.extLineItemId + } + + and: "Line item was used in auction" + verifyAll(lineItemStatus) { + lineItemStatus[0].accountAuctions == lineItemCount + lineItemStatus[0].targetMatched == lineItemCount + lineItemStatus[0].sentToBidder == lineItemCount + lineItemStatus[0].spentTokens == lineItemCount + } + + cleanup: + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should update auction count when register its instance in Planner after auction"() { + given: "Initial auction count" + def initialRequestCount = generalPlanner.requestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } + def initialAuctionCount = generalPlanner.lastRecordedRegisterRequest?.status?.dealsStatus?.clientAuctions + + and: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial Planner request count" + initialRequestCount = generalPlanner.requestCount + + and: "Auction is requested" + pgPbsService.sendAuctionRequest(bidRequest) + + when: "PBS sends request to Planner" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.registerInstanceRequest) + + then: "Request counter is increased" + PBSUtils.waitUntil { generalPlanner.requestCount == initialRequestCount + 1 } + + and: "Delivery Statistics Report has info about auction" + def registerRequest = generalPlanner.lastRecordedRegisterRequest + assert registerRequest.status?.dealsStatus?.clientAuctions == initialAuctionCount + 1 + + cleanup: + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/ReportSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/ReportSpec.groovy new file mode 100644 index 00000000000..6b013e63ec0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/ReportSpec.groovy @@ -0,0 +1,542 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.deals.lineitem.DeliverySchedule +import org.prebid.server.functional.model.deals.lineitem.LineItem +import org.prebid.server.functional.model.deals.lineitem.Token +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils + +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +import static java.time.ZoneOffset.UTC +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 +import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.prebid.server.functional.model.deals.lineitem.LineItem.TIME_PATTERN +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_PASSWORD +import static org.prebid.server.functional.testcontainers.PbsPgConfig.PG_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.HttpUtil.AUTHORIZATION_HEADER +import static org.prebid.server.functional.util.HttpUtil.CHARSET_HEADER_VALUE +import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER +import static org.prebid.server.functional.util.HttpUtil.CONTENT_TYPE_HEADER_VALUE +import static org.prebid.server.functional.util.HttpUtil.PG_TRX_ID_HEADER +import static org.prebid.server.functional.util.HttpUtil.UUID_REGEX + +class ReportSpec extends BasePgSpec { + + def cleanup() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS shouldn't send delivery statistics when PBS doesn't have reports to send"() { + given: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + when: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "Delivery Statistics Service request count is not changed" + assert deliveryStatistics.requestCount == initialRequestCount + } + + def "PBS shouldn't send delivery statistics when delivery report batch is created but doesn't have reports to send"() { + given: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + when: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "Delivery Statistics Service request count is not changed" + assert deliveryStatistics.requestCount == initialRequestCount + } + + def "PBS should send a report request with appropriate headers"() { + given: "Initial report sent request count is taken" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Line items are fetched" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(PBSUtils.randomString)) + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + and: "Delivery report batch is created" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + when: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + and: "PBS sends a report request to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + then: "Request headers corresponds to the payload" + def deliveryRequestHeaders = deliveryStatistics.lastRecordedDeliveryRequestHeaders + assert deliveryRequestHeaders + + and: "Request has an authorization header with a basic auth token" + def basicAuthToken = HttpUtil.makeBasicAuthHeaderValue(PG_ENDPOINT_USERNAME, PG_ENDPOINT_PASSWORD) + assert deliveryRequestHeaders.get(AUTHORIZATION_HEADER) == [basicAuthToken] + + and: "Request has a header with uuid value" + def uuidHeader = deliveryRequestHeaders.get(PG_TRX_ID_HEADER) + assert uuidHeader?.size() == 1 + assert (uuidHeader[0] =~ UUID_REGEX).matches() + + and: "Request has a content type header" + assert deliveryRequestHeaders.get(CONTENT_TYPE_HEADER) == ["$CONTENT_TYPE_HEADER_VALUE;$CHARSET_HEADER_VALUE"] + } + + def "PBS should send delivery statistics report when delivery progress report with one line item is created"() { + given: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Time before report is sent" + def startTime = ZonedDateTime.now(UTC) + + and: "Set Planner response to return one line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(PBSUtils.randomString) + def lineItem = plansResponse.lineItems[0] + generalPlanner.initPlansResponse(plansResponse) + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + when: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends a report request to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + and: "Report request should correspond to the payload" + def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest + def endTime = ZonedDateTime.now(ZoneId.from(UTC)) + + verifyAll(reportRequest) { + (reportRequest.reportId =~ UUID_REGEX).matches() + reportRequest.instanceId == pgConfig.hostId + reportRequest.vendor == pgConfig.vendor + reportRequest.region == pgConfig.region + !reportRequest.clientAuctions + + reportRequest.reportTimeStamp.isBefore(endTime) + reportRequest.dataWindowStartTimeStamp.isBefore(startTime) + reportRequest.dataWindowEndTimeStamp.isAfter(startTime) + reportRequest.dataWindowEndTimeStamp.isBefore(endTime) + reportRequest.reportTimeStamp.isAfter(reportRequest.dataWindowEndTimeStamp) + } + + and: "Report line items should have an appropriate to the initially set line items info" + assert reportRequest.lineItemStatus?.size() == 1 + def lineItemStatus = reportRequest.lineItemStatus[0] + + verifyAll(lineItemStatus) { + lineItemStatus.lineItemSource == lineItem.source + lineItemStatus.lineItemId == lineItem.lineItemId + lineItemStatus.dealId == lineItem.dealId + lineItemStatus.extLineItemId == lineItem.extLineItemId + !lineItemStatus.accountAuctions + !lineItemStatus.domainMatched + !lineItemStatus.targetMatched + !lineItemStatus.targetMatchedButFcapped + !lineItemStatus.targetMatchedButFcapLookupFailed + !lineItemStatus.pacingDeferred + !lineItemStatus.sentToBidder + !lineItemStatus.sentToBidderAsTopMatch + !lineItemStatus.receivedFromBidder + !lineItemStatus.receivedFromBidderInvalidated + !lineItemStatus.sentToClient + !lineItemStatus.sentToClientAsTopMatch + !lineItemStatus.lostToLineItems + !lineItemStatus.events + !lineItemStatus.readyAt + !lineItemStatus.spentTokens + !lineItemStatus.pacingFrequency + + lineItemStatus.deliverySchedule?.size() == 1 + } + + def timeFormatter = DateTimeFormatter.ofPattern(TIME_PATTERN) + def deliverySchedule = lineItemStatus.deliverySchedule[0] + + verifyAll(deliverySchedule) { + deliverySchedule.planId == lineItem.deliverySchedules[0].planId + timeFormatter.format(deliverySchedule.planStartTimeStamp) == + timeFormatter.format(lineItem.deliverySchedules[0].startTimeStamp) + timeFormatter.format(deliverySchedule.planUpdatedTimeStamp) == + timeFormatter.format(lineItem.deliverySchedules[0].updatedTimeStamp) + timeFormatter.format(deliverySchedule.planExpirationTimeStamp) == + timeFormatter.format(lineItem.deliverySchedules[0].endTimeStamp) + + deliverySchedule.tokens?.size() == 1 + } + + verifyAll(deliverySchedule.tokens[0]) { tokens -> + tokens.priorityClass == lineItem.deliverySchedules[0].tokens[0].priorityClass + tokens.total == lineItem.deliverySchedules[0].tokens[0].total + tokens.spent == 0 + tokens.totalSpent == 0 + } + } + + def "PBS should send a correct delivery statistics report when auction with one line item is happened"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Time before report is sent" + def startTime = ZonedDateTime.now(UTC) + + and: "Set Planner response to return one line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + def lineItem = plansResponse.lineItems[0] + def lineItemCount = plansResponse.lineItems.size() as Long + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + when: "Auction request to PBS is sent" + pgPbsService.sendAuctionRequest(bidRequest) + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + and: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends a report request to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + and: "Report request should be sent after the test start" + def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest + assert reportRequest.reportTimeStamp.isAfter(startTime) + + and: "Request should contain correct number of client auctions made" + assert reportRequest.clientAuctions == 1 + + and: "Report line items should have an appropriate to the initially set line item info" + assert reportRequest.lineItemStatus?.size() == 1 + def lineItemStatus = reportRequest.lineItemStatus[0] + + verifyAll(lineItemStatus) { + lineItemStatus.lineItemSource == lineItem.source + lineItemStatus.lineItemId == lineItem.lineItemId + lineItemStatus.dealId == lineItem.dealId + lineItemStatus.extLineItemId == lineItem.extLineItemId + } + + and: "Report should have the right PG metrics info" + verifyAll(lineItemStatus) { + lineItemStatus?.accountAuctions == lineItemCount + lineItemStatus?.targetMatched == lineItemCount + lineItemStatus?.sentToBidder == lineItemCount + lineItemStatus?.sentToBidderAsTopMatch == lineItemCount + } + + and: "Report line item should have a delivery schedule" + assert lineItemStatus.deliverySchedule?.size() == 1 + assert lineItemStatus.deliverySchedule[0].planId == lineItem.deliverySchedules[0].planId + } + + def "PBS should use line item token with the highest priority"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Time before report is sent" + def startTime = ZonedDateTime.now(UTC) + + and: "Set Planner response to return one line item" + def highestPriorityToken = new Token(priorityClass: 1, total: 2) + def lowerPriorityToken = new Token(priorityClass: 3, total: 2) + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + def tokens = [highestPriorityToken, lowerPriorityToken] + lineItems[0].deliverySchedules[0].tokens = tokens + } + def tokens = plansResponse.lineItems[0].deliverySchedules[0].tokens + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + when: "Auction request to PBS is sent" + pgPbsService.sendAuctionRequest(bidRequest) + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + and: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends a report request to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + and: "Report request should be sent after the test start" + def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest + assert reportRequest.reportTimeStamp.isAfter(startTime) + + and: "Token with the highest priority was used" + def reportTokens = reportRequest.lineItemStatus?.first()?.deliverySchedule?.first()?.tokens + assert reportTokens + assert reportTokens.size() == tokens.size() + def usedToken = reportTokens.find { it.priorityClass == highestPriorityToken.priorityClass } + assert usedToken?.total == highestPriorityToken.total + assert usedToken?.spent == 1 + assert usedToken?.totalSpent == 1 + + and: "Token with a lower priority wasn't used" + def notUsedToken = reportTokens.find { it.priorityClass == lowerPriorityToken.priorityClass } + assert notUsedToken?.total == lowerPriorityToken.total + assert notUsedToken?.spent == 0 + assert notUsedToken?.totalSpent == 0 + } + + def "PBS shouldn't consider line item as used when bidder responds with non-deals specific info"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Time before report is sent" + def startTime = ZonedDateTime.now(UTC) + + and: "Set Planner response to return one line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Non-deals bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + when: "Auction request to PBS is sent" + pgPbsService.sendAuctionRequest(bidRequest) + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + and: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends a report request to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + and: "Report request should be sent after the test start" + def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest + assert reportRequest.reportTimeStamp.isAfter(startTime) + + and: "Line item token wasn't used" + def reportTokens = reportRequest.lineItemStatus?.first()?.deliverySchedule?.first()?.tokens + assert reportTokens?.size() == plansResponse.lineItems[0].deliverySchedules[0].tokens.size() + assert reportTokens[0].spent == 0 + assert reportTokens[0].totalSpent == 0 + } + + def "PBS should send additional report when line items number exceeds PBS 'line-items-per-report' property"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + def accountId = bidRequest.site.publisher.id + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Already recorded request count is reset" + deliveryStatistics.resetRecordedRequests() + deliveryStatistics.setResponse() + + and: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Set Planner response to return #lineItemsPerReport + 1 line items" + def lineItemsPerReport = pgConfig.lineItemsPerReport + def plansResponse = new PlansResponse(lineItems: (1..lineItemsPerReport + 1).collect { + LineItem.getDefaultLineItem(accountId) + }) + generalPlanner.initPlansResponse(plansResponse) + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + when: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + and: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends two report requests to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 2 } + + and: "Two reports are sent" + def reportRequests = deliveryStatistics.recordedDeliveryStatisticsReportRequests + assert reportRequests.size() == 2 + + and: "Two reports were sent with #lineItemsPerReport and 1 number of line items" + assert [reportRequests[-2].lineItemStatus.size(), reportRequests[-1].lineItemStatus.size()].sort() == + [lineItemsPerReport, 1].sort() + } + + def "PBS should save reports for later sending when response from Delivery Statistics was unsuccessful"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + def accountId = bidRequest.site.publisher.id + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + + and: "Set Planner response to return 1 line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(accountId) + generalPlanner.initPlansResponse(plansResponse) + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + and: "Delivery Statistics Service response is set to return a bad status code" + deliveryStatistics.reset() + deliveryStatistics.setResponse(INTERNAL_SERVER_ERROR_500) + + when: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends a report to Delivery Statistics" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + when: "Delivery Statistics Service response is set to return a success response" + deliveryStatistics.reset() + deliveryStatistics.setResponse(OK_200) + + and: "PBS is requested to send a report to Delivery Statistics for the second time" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS for the second time sends the same report to the Delivery Statistics Service" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 2 } + } + + def "PBS should change active delivery plan when the current plan lifetime expires"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + def accountId = bidRequest.site.publisher.id + + and: "Initial Delivery Statistics Service request count" + def initialRequestCount = deliveryStatistics.requestCount + def auctionCount = 2 + + and: "Current delivery plan which expires in 2 seconds" + def currentPlanTimeToLive = 2 + def currentDeliverySchedule = new DeliverySchedule(planId: PBSUtils.randomNumber as String, + startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), + updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)), + endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(currentPlanTimeToLive), + tokens: [new Token(priorityClass: 1, total: 1000)]) + + and: "Next delivery plan" + def nextDeliverySchedule = new DeliverySchedule(planId: PBSUtils.randomNumber as String, + startTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(currentPlanTimeToLive), + updatedTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(currentPlanTimeToLive), + endTimeStamp: ZonedDateTime.now(ZoneId.from(UTC)).plusHours(1), + tokens: [new Token(priorityClass: 1, total: 500)]) + + and: "Set Planner response to return line item with two delivery plans" + def plansResponse = PlansResponse.getDefaultPlansResponse(accountId).tap { + lineItems[0].deliverySchedules = [currentDeliverySchedule, nextDeliverySchedule] + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "PBS requests Planner line items" + def initialPlansRequestCount = generalPlanner.recordedPlansRequestCount + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + PBSUtils.waitUntil { generalPlanner.recordedPlansRequestCount == initialPlansRequestCount + 1 } + + and: "Auction request to PBS is sent for the first time" + pgPbsService.sendAuctionRequest(bidRequest) + + when: "Current delivery plan lifetime is expired" + PBSUtils.waitUntil({ ZonedDateTime.now(ZoneId.from(UTC)).isAfter(currentDeliverySchedule.endTimeStamp) }, + (currentPlanTimeToLive * 1000) + 1000) + + and: "PBS requests Planner line items which also forces current PBS line items to be updated" + generalPlanner.initPlansResponse(plansResponse) + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Auction request to PBS is sent for the second time" + bidder.setResponse(bidRequest.id, bidResponse) + pgPbsService.sendAuctionRequest(bidRequest) + + and: "PBS generates delivery report batch" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.createReportRequest) + + and: "PBS is requested to send a report to Delivery Statistics" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.sendReportRequest) + + then: "PBS sends a report to Delivery Statistics" + PBSUtils.waitUntil { deliveryStatistics.requestCount == initialRequestCount + 1 } + + and: "Report has info about 2 happened auctions" + def reportRequest = deliveryStatistics.lastRecordedDeliveryStatisticsReportRequest + assert reportRequest.clientAuctions == auctionCount + assert reportRequest.lineItemStatus?.size() == plansResponse.lineItems.size() + assert reportRequest.lineItemStatus[0].accountAuctions == auctionCount + + and: "One line item during each auction was sent to the bidder" + assert reportRequest.lineItemStatus[0].sentToBidder == auctionCount + + and: "Report contains two delivery plans info" + def reportDeliverySchedules = reportRequest.lineItemStatus[0].deliverySchedule + assert reportDeliverySchedules?.size() == plansResponse.lineItems[0].deliverySchedules.size() + + and: "One token was used during the first auction by the first delivery plan" + assert reportDeliverySchedules.find { it.planId == currentDeliverySchedule.planId }?.tokens[0].spent == 1 + + and: "One token was used from another delivery plan during the second auction after first delivery plan lifetime expired" + assert reportDeliverySchedules.find { it.planId == nextDeliverySchedule.planId }?.tokens[0].spent == 1 + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy new file mode 100644 index 00000000000..c8ee2910bb7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/TargetingSpec.groovy @@ -0,0 +1,683 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.bidder.Rubicon +import org.prebid.server.functional.model.deals.lineitem.LineItemSize +import org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator +import org.prebid.server.functional.model.deals.lineitem.targeting.Targeting +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.App +import org.prebid.server.functional.model.request.auction.AppExt +import org.prebid.server.functional.model.request.auction.AppExtData +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Bidder +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpExt +import org.prebid.server.functional.model.request.auction.ImpExtContext +import org.prebid.server.functional.model.request.auction.ImpExtContextData +import org.prebid.server.functional.model.request.auction.Publisher +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.model.request.auction.SiteExt +import org.prebid.server.functional.model.request.auction.SiteExtData +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.model.request.auction.UserExt +import org.prebid.server.functional.model.request.auction.UserExtData +import org.prebid.server.functional.model.request.auction.UserTime +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Shared +import spock.lang.Unroll + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC +import static java.time.temporal.WeekFields.SUNDAY_START +import static org.prebid.server.functional.model.bidder.BidderName.RUBICON +import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.NOT +import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.OR +import static org.prebid.server.functional.model.deals.lineitem.targeting.BooleanOperator.UPPERCASE_AND +import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.IN +import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.INTERSECTS +import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.MATCHES +import static org.prebid.server.functional.model.deals.lineitem.targeting.MatchingFunction.WITHIN +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_AD_SLOT +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_MEDIA_TYPE +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.AD_UNIT_SIZE +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.APP_BUNDLE +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.BIDP_ACCOUNT_ID +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.DOW +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.HOUR +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.INVALID +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.PAGE_POSITION +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.REFERRER +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_BUYER_ID +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_BUYER_IDS +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_KEYWORDS +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SFPD_LANGUAGE +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.SITE_DOMAIN +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_BUYER_ID +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_BUYER_IDS +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_KEYWORDS +import static org.prebid.server.functional.model.deals.lineitem.targeting.TargetingType.UFPD_LANGUAGE +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class TargetingSpec extends BasePgSpec { + + @Shared + String stringTargetingValue = PBSUtils.randomString + @Shared + Integer integerTargetingValue = PBSUtils.randomNumber + + def cleanup() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + @Unroll + def "PBS should invalidate line items when targeting has #reason"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = targeting + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS hasn't had PG deals auction as line item hasn't passed validation" + assert !auctionResponse.ext?.debug?.pgmetrics + + where: + reason | targeting + + "two root nodes" | Targeting.invalidTwoRootNodesTargeting + + "invalid boolean operator" | new Targeting.Builder(BooleanOperator.INVALID).addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) + .build() + + "uppercase boolean operator" | new Targeting.Builder(UPPERCASE_AND).addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) + .build() + + "invalid targeting type" | Targeting.defaultTargetingBuilder + .addTargeting(INVALID, INTERSECTS, [PBSUtils.randomString]) + .build() + + "'in' matching type value as not list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, IN, BANNER) + .build() + + "'intersects' matching type value as not list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, BANNER) + .build() + + "'within' matching type value as not list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, WITHIN, BANNER) + .build() + + "'matches' matching type value as list" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, MATCHES, [BANNER]) + .build() + + "null targeting height and width" | new Targeting.Builder().addTargeting(AD_UNIT_SIZE, INTERSECTS, [new LineItemSize(w: null, h: null)]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [BANNER]) + .build() + } + + @Unroll + def "PBS should invalidate line items with not supported '#matchingFunction' matching function by '#targetingType' targeting type"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(targetingType, matchingFunction, [PBSUtils.randomString]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS hasn't had PG deals auction as line item hasn't passed validation" + assert !auctionResponse.ext?.debug?.pgmetrics + + where: + matchingFunction | targetingType + INTERSECTS | SITE_DOMAIN + WITHIN | SITE_DOMAIN + INTERSECTS | REFERRER + WITHIN | REFERRER + INTERSECTS | APP_BUNDLE + WITHIN | APP_BUNDLE + INTERSECTS | AD_UNIT_AD_SLOT + WITHIN | AD_UNIT_AD_SLOT + } + + @Unroll + def "PBS should support line item targeting by string '#targetingType' targeting type"() { + given: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(targetingType, MATCHES, stringTargetingValue) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + + where: + targetingType | bidRequest + + REFERRER | BidRequest.defaultBidRequest.tap { + site.page = stringTargetingValue + } + + APP_BUNDLE | BidRequest.defaultBidRequest.tap { + app = App.defaultApp.tap { bundle = stringTargetingValue } + } + + UFPD_LANGUAGE | BidRequest.defaultBidRequest.tap { + user = User.defaultUser.tap { + language = stringTargetingValue + } + } + } + + @Unroll + def "PBS should support both scalar and array String inputs by User First Party Data '#targetingType' for intersects matching function"() { + given: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(targetingType, INTERSECTS, [stringTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + + where: + targetingType | bidRequest + + UFPD_LANGUAGE | BidRequest.defaultBidRequest.tap { + user = User.defaultUser.tap { + ext = new UserExt(data: new UserExtData(language: stringTargetingValue)) + } + } + + UFPD_KEYWORDS | BidRequest.defaultBidRequest.tap { + user = User.defaultUser.tap { + ext = new UserExt(data: new UserExtData(keywords: [stringTargetingValue])) + } + } + } + + @Unroll + def "PBS should support both scalar and array Integer inputs by User First Party Data '#targetingType' for intersects matching function"() { + given: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(targetingType, INTERSECTS, [integerTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + + where: + targetingType | bidRequest + + UFPD_BUYER_ID | BidRequest.defaultBidRequest.tap { + user = User.defaultUser.tap { + ext = new UserExt(data: new UserExtData(buyerid: integerTargetingValue)) + } + } + + UFPD_BUYER_IDS | BidRequest.defaultBidRequest.tap { + user = User.defaultUser.tap { + ext = new UserExt(data: new UserExtData(buyerids: [integerTargetingValue])) + } + } + } + + @Unroll + def "PBS should support taking Site First Party Data from 3 different sources"() { + given: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(SFPD_LANGUAGE, INTERSECTS, [stringTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + + where: + bidRequest << [ + BidRequest.defaultBidRequest.tap { + imp = [Imp.defaultImpression.tap { + banner = Banner.defaultBanner + ext.context = new ImpExtContext(data: new ImpExtContextData(language: stringTargetingValue)) + }] + }, + BidRequest.defaultBidRequest.tap { + site = Site.defaultSite.tap { + ext = new SiteExt(data: new SiteExtData(language: stringTargetingValue)) + } + }, + BidRequest.defaultBidRequest.tap { + app = App.defaultApp.tap { + ext = new AppExt(data: new AppExtData(language: stringTargetingValue)) + } + } + ] + } + + def "PBS should support String array input for Site First Party Data to be matched by intersects matching function"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [Imp.defaultImpression.tap { + banner = Banner.defaultBanner + ext.context = new ImpExtContext(data: new ImpExtContextData(keywords: [stringTargetingValue])) + }] + } + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(SFPD_KEYWORDS, INTERSECTS, [stringTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + } + + @Unroll + def "PBS should support both scalar and array Integer inputs in Site First Party Data ('#targetingType') by intersects matching function"() { + given: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(targetingType, INTERSECTS, [integerTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + + where: + targetingType | bidRequest + SFPD_BUYER_ID | BidRequest.defaultBidRequest.tap { + imp = [Imp.defaultImpression.tap { + banner = Banner.defaultBanner + ext.context = new ImpExtContext(data: new ImpExtContextData(buyerid: integerTargetingValue)) + }] + } + + SFPD_BUYER_IDS | BidRequest.defaultBidRequest.tap { + imp = [Imp.defaultImpression.tap { + banner = Banner.defaultBanner + ext.context = new ImpExtContext(data: new ImpExtContextData(buyerids: [integerTargetingValue])) + }] + } + } + + def "PBS should support targeting matching by bidder parameters"() { + given: "Bid request with specified bidder parameter" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [Imp.defaultImpression.tap { + banner = Banner.defaultBanner + ext = ImpExt.defaultImpExt + ext.prebid.bidder = new Bidder(rubicon: Rubicon.default.tap { accountId = integerTargetingValue }) + }] + } + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].source = RUBICON.name().toLowerCase() + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(BIDP_ACCOUNT_ID, INTERSECTS, [integerTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + } + + @Unroll + def "PBS doesn't throw a NPE for '#targetingType' when its Ext is absent and targeting Intersects matching type is selected"() { + given: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(targetingType, INTERSECTS, [stringTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS successfully processed request" + notThrown(PrebidServerException) + + and: "PBS hasn't had PG auction as request targeting is not specified in the right place" + assert !auctionResponse.ext?.debug?.pgmetrics + + where: + targetingType | bidRequest + SFPD_KEYWORDS | BidRequest.defaultBidRequest.tap { + site = Site.defaultSite.tap { + keywords = stringTargetingValue + } + } + + UFPD_LANGUAGE | BidRequest.defaultBidRequest.tap { + user = User.defaultUser.tap { + language = stringTargetingValue + } + } + } + + def "PBS should support line item targeting by page position targeting type"() { + given: "Bid request and bid response" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.pos = integerTargetingValue + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(PAGE_POSITION, IN, [integerTargetingValue]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + } + + def "PBS should support line item targeting by userdow targeting type"() { + given: "Bid request and bid response" + def bidRequest = BidRequest.defaultBidRequest.tap { + def weekDay = ZonedDateTime.now(ZoneId.from(UTC)).dayOfWeek.get(SUNDAY_START.dayOfWeek()) + user = User.defaultUser.tap { + ext = new UserExt(time: new UserTime(userdow: weekDay)) + } + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(DOW, IN, [ZonedDateTime.now(ZoneId.from(UTC)).dayOfWeek.get(SUNDAY_START.dayOfWeek())]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + } + + def "PBS should support line item targeting by userhour targeting type"() { + given: "Bid request and bid response" + def bidRequest = BidRequest.defaultBidRequest.tap { + def hour = ZonedDateTime.now(ZoneId.from(UTC)).hour + user = User.defaultUser.tap { + ext = new UserExt(time: new UserTime(userhour: hour)) + } + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(HOUR, IN, [ZonedDateTime.now(ZoneId.from(UTC)).hour]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + } + + def "PBS should support line item targeting by '#targetingType' targeting type"() { + given: "Bid request and bid response" + def bidRequest = BidRequest.defaultBidRequest + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(HOUR, IN, [ZonedDateTime.now(ZoneId.from(UTC)).hour]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == plansResponse.lineItems.size() + + where: + targetingType | targetingValue + + "'\$or' root node with one match" | new Targeting.Builder(OR).addTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) + .build() + + "'\$not' root node without matches" | new Targeting.Builder(NOT).buildNotBooleanOperatorTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) + } + + @Unroll + def "PBS should support line item domain targeting by #domainTargetingType"() { + given: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(SITE_DOMAIN, MATCHES, stringTargetingValue) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + def lineItemSize = plansResponse.lineItems.size() + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize + + and: "Targeting recorded as matched" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedDomainTargeting?.size() == lineItemSize + + where: + domainTargetingType | bidRequest + + "site domain" | BidRequest.defaultBidRequest.tap { + site.domain = stringTargetingValue + } + + "site publisher domain" | BidRequest.defaultBidRequest.tap { + site.publisher = Publisher.defaultPublisher.tap { domain = stringTargetingValue } + } + } + + @Unroll + def "PBS should support line item domain targeting"() { + given: "Bid response" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.domain = siteDomain + site.publisher = Publisher.defaultPublisher.tap { domain = sitePublisherDomain } + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = Targeting.defaultTargetingBuilder + .addTargeting(SITE_DOMAIN, IN, [siteDomain]) + .build() + } + generalPlanner.initPlansResponse(plansResponse) + def lineItemSize = plansResponse.lineItems.size() + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS had PG auction" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedWholeTargeting?.size() == lineItemSize + + and: "Targeting recorded as matched" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedDomainTargeting?.size() == lineItemSize + + where: + siteDomain | sitePublisherDomain + "www.example.com" | null + "https://www.example.com" | null + "www.example.com" | "example.com" + } + + @Unroll + def "PBS should appropriately match '\$or', '\$not' line items targeting root node rules"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner response" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].targeting = targeting + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS hasn't had PG deals auction as targeting differs" + assert !auctionResponse.ext?.debug?.pgmetrics + + where: + targeting << [new Targeting.Builder(OR).addTargeting(AD_UNIT_SIZE, INTERSECTS, [new LineItemSize(w: PBSUtils.randomNumber, h: PBSUtils.randomNumber)]) + .addTargeting(AD_UNIT_MEDIA_TYPE, INTERSECTS, [VIDEO]) + .build(), + new Targeting.Builder(NOT).buildNotBooleanOperatorTargeting(AD_UNIT_SIZE, INTERSECTS, [LineItemSize.defaultLineItemSize])] + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/TokenSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/TokenSpec.groovy new file mode 100644 index 00000000000..7d78b80e32f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/TokenSpec.groovy @@ -0,0 +1,239 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.deals.lineitem.Token +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.response.auction.BidResponse + +import java.time.ZoneId +import java.time.ZonedDateTime + +import static java.time.ZoneOffset.UTC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC + +class TokenSpec extends BasePgSpec { + + def cleanup() { + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.invalidateLineItemsRequest) + } + + def "PBS should start using line item in auction when its expired tokens number is increased"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock zero tokens line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].deliverySchedules[0].tokens[0].total = 0 + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is requested" + def firstAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start processing PG deals" + assert firstAuctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == + [plansResponse.lineItems[0].lineItemId] as Set + assert !firstAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder + + when: "Line item tokens are updated" + plansResponse.lineItems[0].deliverySchedules[0].tokens[0].total = 1 + plansResponse.lineItems[0].deliverySchedules[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).plusSeconds(1) + generalPlanner.initPlansResponse(plansResponse) + + and: "Updated line items are fetched by PBS" + updateLineItemsAndWait() + + and: "Auction is requested for the second time" + bidder.setResponse(bidRequest.id, bidResponse) + def secondAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should process PG deals" + def sentToBidder = secondAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == plansResponse.lineItems.size() + assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId + } + + def "PBS shouldn't allow line items with zero token number take part in auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock zero tokens line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].deliverySchedules[0].tokens[0].total = 0 + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should recognize line items with pacing deferred" + assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == [plansResponse.lineItems[0].lineItemId] as Set + } + + def "PBS should allow line item take part in auction when it has at least one unspent token among all expired tokens"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line item with zero and 1 available tokens" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + def deliverySchedules = lineItems[0].deliverySchedules[0] + deliverySchedules.tokens[0].total = 0 + deliverySchedules.tokens << new Token(priorityClass: 2, total: 0) + deliverySchedules.tokens << new Token(priorityClass: 3, total: 1) + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + def lineItemCount = plansResponse.lineItems.size() + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should process PG deals" + assert !auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred + assert auctionResponse.ext?.debug?.pgmetrics?.readyToServe?.size() == lineItemCount + def sentToBidder = auctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == lineItemCount + assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId + } + + def "PBS shouldn't allow line item take part in auction when all its tokens are spent"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock with 1 token to spend line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].deliverySchedules[0].tokens[0].total = 1 + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + and: "Auction is happened for the first time" + pgPbsService.sendAuctionRequest(bidRequest) + + when: "Requesting auction for the second time" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start PG processing" + assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == [plansResponse.lineItems[0].lineItemId] as Set + } + + def "PBS should take only the first token among tokens with the same priority class"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line item with 2 tokens of the same priority but the first has zero total number" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + def tokens = [new Token(priorityClass: 1, total: 0), new Token(priorityClass: 1, total: 1)] + lineItems[0].deliverySchedules[0].tokens = tokens + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is happened" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start PG processing as it was processed only the first token with 0 total number" + assert auctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == [plansResponse.lineItems[0].lineItemId] as Set + } + + def "PBS shouldn't allow line item take part in auction when its number of available impressions is ahead of the scheduled time"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line item to have max 2 impressions during one week" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].deliverySchedules[0].tokens[0].total = 2 + lineItems[0].startTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) + lineItems[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) + lineItems[0].endTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)).plusWeeks(1) + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is requested for the first time" + def firstAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS processed PG deals" + def sentToBidder = firstAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == plansResponse.lineItems.size() + assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId + + when: "Auction is requested for the second time" + def secondAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS hasn't allowed line item take part in auction as it has only one impression left to be shown during the week" + assert secondAuctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == + [plansResponse.lineItems[0].lineItemId] as Set + assert !secondAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder + } + + def "PBS should abandon line item with updated zero available token number take part in auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock with not null tokens number line item" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].deliverySchedules[0].tokens[0].total = 2 + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + updateLineItemsAndWait() + + when: "Auction is requested" + def firstAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS processed PG deals" + def sentToBidder = firstAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder?.get(GENERIC.value) + assert sentToBidder?.size() == plansResponse.lineItems.size() + assert sentToBidder[0] == plansResponse.lineItems[0].lineItemId + + when: "Line item tokens are updated to have no available tokens" + plansResponse.lineItems[0].deliverySchedules[0].tokens[0].total = 0 + plansResponse.lineItems[0].deliverySchedules[0].updatedTimeStamp = ZonedDateTime.now(ZoneId.from(UTC)) + + generalPlanner.initPlansResponse(plansResponse) + + and: "Updated line items are fetched by PBS" + updateLineItemsAndWait() + + and: "Auction is requested for the second time" + def secondAuctionResponse = pgPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't start processing PG deals" + assert secondAuctionResponse.ext?.debug?.pgmetrics?.pacingDeferred == + [plansResponse.lineItems[0].lineItemId] as Set + assert !secondAuctionResponse.ext?.debug?.pgmetrics?.sentToBidder + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pg/UserDetailsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pg/UserDetailsSpec.groovy new file mode 100644 index 00000000000..0fd280c6f3d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pg/UserDetailsSpec.groovy @@ -0,0 +1,350 @@ +package org.prebid.server.functional.tests.pg + +import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.model.deals.lineitem.FrequencyCap +import org.prebid.server.functional.model.deals.userdata.UserDetailsResponse +import org.prebid.server.functional.model.mock.services.generalplanner.PlansResponse +import org.prebid.server.functional.model.mock.services.httpsettings.HttpAccountsResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.dealsupdate.ForceDealsUpdateRequest +import org.prebid.server.functional.model.request.event.EventRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.testcontainers.Dependencies +import org.prebid.server.functional.testcontainers.scaffolding.HttpSettings +import org.prebid.server.functional.util.HttpUtil +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Shared +import spock.lang.Unroll + +import java.time.format.DateTimeFormatter + +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 +import static org.mockserver.model.HttpStatusCode.NOT_FOUND_404 +import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.deals.lineitem.LineItem.TIME_PATTERN + +class UserDetailsSpec extends BasePgSpec { + + private static final String USER_SERVICE_NAME = "userservice" + + @Shared + HttpSettings httpSettings = new HttpSettings(Dependencies.networkServiceContainer, mapper) + + def "PBS should send user details request to the User Service during deals auction"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial user details request count is taken" + def initialRequestCount = userData.recordedUserDetailsRequestCount + + and: "Cookies with user ids" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS sends a request to the User Service" + def updatedRequestCount = userData.recordedUserDetailsRequestCount + assert updatedRequestCount == initialRequestCount + 1 + + and: "Request corresponds to the payload" + def userDetailsRequest = userData.recordedUserDetailsRequest + assert userDetailsRequest.time?.isAfter(uidsCookie.bday) + assert userDetailsRequest.ids?.size() == 1 + assert userDetailsRequest.ids[0].id == uidsCookie.tempUIDs.get(GENERIC.value).uid + assert userDetailsRequest.ids[0].type == pgConfig.userIdType + } + + @Unroll + def "PBS should validate bad user details response status code ('#statusCode')"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial user details request count is taken" + def initialRequestCount = userData.recordedUserDetailsRequestCount + + and: "User Service response is set" + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse, statusCode) + + and: "Cookies with user ids" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS sends a request to the User Service during auction" + assert userData.recordedUserDetailsRequestCount == initialRequestCount + 1 + def userServiceCall = auctionResponse.ext?.debug?.httpcalls?.get(USER_SERVICE_NAME) + assert userServiceCall?.size() == 1 + + assert !userServiceCall[0].status + assert !userServiceCall[0].responsebody + + cleanup: + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) + + where: + statusCode << [NO_CONTENT_204, NOT_FOUND_404, INTERNAL_SERVER_ERROR_500] + } + + @Unroll + def "PBS should invalidate user details response body when response has absent #fieldName field"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial user details request count is taken" + def initialRequestCount = userData.recordedUserDetailsRequestCount + + and: "User Service response is set" + userData.setUserDataResponse(userDataResponse) + + and: "Cookies with user ids" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS sends a request to the User Service" + assert userData.recordedUserDetailsRequestCount == initialRequestCount + 1 + + and: "Call to the user service was made" + assert auctionResponse.ext?.debug?.httpcalls?.get(USER_SERVICE_NAME)?.size() == 1 + + and: "Data from the user service response wasn't added to the bid request by PBS" + assert !auctionResponse.ext?.debug?.resolvedrequest?.user?.data + assert !auctionResponse.ext?.debug?.resolvedrequest?.user?.ext?.fcapids + + cleanup: + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) + + where: + fieldName | userDataResponse + "user" | new UserDetailsResponse(user: null) + "user.data" | UserDetailsResponse.defaultUserResponse.tap { user.data = null } + "user.ext" | UserDetailsResponse.defaultUserResponse.tap { user.ext = null } + } + + def "PBS should abandon line items with user fCap ids take part in auction when user details response failed"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Planner Mock line items with added frequency cap" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id).tap { + lineItems[0].frequencyCaps = [FrequencyCap.defaultFrequencyCap.tap { fcapId = PBSUtils.randomNumber as String }] + } + generalPlanner.initPlansResponse(plansResponse) + + and: "Bid response" + def bidResponse = BidResponse.getDefaultPgBidResponse(bidRequest, plansResponse) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Bad User Service Response is set" + userData.setUserDataResponse(new UserDetailsResponse(user: null)) + + and: "Cookies header" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS" + def auctionResponse = pgPbsService.sendAuctionRequest(bidRequest, cookieHeader) + + then: "PBS hasn't started processing PG deals as line item targeting frequency capped lookup failed" + assert auctionResponse.ext?.debug?.pgmetrics?.matchedTargetingFcapLookupFailed?.size() == + plansResponse.lineItems.size() + + cleanup: + userData.setUserDataResponse(UserDetailsResponse.defaultUserResponse) + } + + def "PBS should send win notification request to the User Service on bidder wins"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + def lineItemId = plansResponse.lineItems[0].lineItemId + def lineItemUpdateTime = plansResponse.lineItems[0].updatedTimeStamp + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial win notification request count" + def initialRequestCount = userData.requestCount + + and: "Enabled event request" + def winEventRequest = EventRequest.defaultEventRequest.tap { + it.lineItemId = lineItemId + analytics = 0 + } + + and: "Default account response" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(winEventRequest.accountId.toString()) + httpSettings.setResponse(winEventRequest.accountId.toString(), httpSettingsResponse) + + and: "Cookies header" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS where the winner is instantiated" + pgPbsService.sendAuctionRequest(bidRequest) + + and: "Sending event request to PBS" + pgPbsService.sendEventRequest(winEventRequest, cookieHeader) + + then: "PBS sends a win notification to the User Service" + PBSUtils.waitUntil { userData.requestCount == initialRequestCount + 1 } + + and: "Win request corresponds to the payload" + def timeFormatter = DateTimeFormatter.ofPattern(TIME_PATTERN) + + verifyAll(userData.recordedWinEventRequest) { winNotificationRequest -> + winNotificationRequest.bidderCode == GENERIC.value + winNotificationRequest.bidId == winEventRequest.bidId + winNotificationRequest.lineItemId == lineItemId + winNotificationRequest.region == pgConfig.region + winNotificationRequest.userIds?.size() == 1 + winNotificationRequest.userIds[0].id == uidsCookie.tempUIDs.get(GENERIC.value).uid + winNotificationRequest.userIds[0].type == pgConfig.userIdType + timeFormatter.format(winNotificationRequest.lineUpdatedDateTime) == timeFormatter.format(lineItemUpdateTime) + winNotificationRequest.winEventDateTime.isAfter(winNotificationRequest.lineUpdatedDateTime) + !winNotificationRequest.frequencyCaps + } + } + + @Unroll + def "PBS shouldn't send win notification request to the User Service when #reason line item id is given"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock line items" + generalPlanner.initPlansResponse(PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id)) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial win notification request count" + def initialRequestCount = userData.requestCount + + and: "Enabled event request" + def eventRequest = EventRequest.defaultEventRequest.tap { + it.lineItemId = lineItemId + analytics = 0 + } + + and: "Default account response" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(eventRequest.accountId.toString()) + httpSettings.setResponse(eventRequest.accountId.toString(), httpSettingsResponse) + + and: "Cookies header" + def uidsCookie = UidsCookie.defaultUidsCookie + def cookieHeader = HttpUtil.getCookieHeader(mapper, uidsCookie) + + when: "Sending auction request to PBS where the winner is instantiated" + pgPbsService.sendAuctionRequest(bidRequest) + + and: "Sending event request to PBS" + pgPbsService.sendEventRequest(eventRequest, cookieHeader) + + then: "PBS hasn't sent a win notification to the User Service" + assert userData.requestCount == initialRequestCount + + where: + reason | lineItemId + "null" | null + "non-existent" | PBSUtils.randomNumber as String + } + + @Unroll + def "PBS shouldn't send win notification request to the User Service when #reason cookies header was given"() { + given: "Bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Planner Mock line items" + def plansResponse = PlansResponse.getDefaultPlansResponse(bidRequest.site.publisher.id) + def lineItemId = plansResponse.lineItems[0].lineItemId + generalPlanner.initPlansResponse(plansResponse) + + and: "Line items are fetched by PBS" + pgPbsService.sendForceDealsUpdateRequest(ForceDealsUpdateRequest.updateLineItemsRequest) + + and: "Initial win notification request count" + def initialRequestCount = userData.requestCount + + and: "Enabled event request" + def eventRequest = EventRequest.defaultEventRequest.tap { + it.lineItemId = lineItemId + analytics = 0 + } + + and: "Default account response" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(eventRequest.accountId.toString()) + httpSettings.setResponse(eventRequest.accountId.toString(), httpSettingsResponse) + + when: "Sending auction request to PBS where the winner is instantiated" + pgPbsService.sendAuctionRequest(bidRequest) + + and: "Sending event request to PBS" + pgPbsService.sendEventRequest(eventRequest, HttpUtil.getCookieHeader(mapper, uidsCookie)) + + then: "PBS hasn't sent a win notification to the User Service" + assert userData.requestCount == initialRequestCount + + where: + reason | uidsCookie + + "empty cookie" | new UidsCookie() + + "empty uids cookie" | UidsCookie.defaultUidsCookie.tap { + uids = null + tempUIDs = null + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy b/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy new file mode 100644 index 00000000000..c03dc7736fb --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/util/HttpUtil.groovy @@ -0,0 +1,33 @@ +package org.prebid.server.functional.util + +import org.prebid.server.functional.model.UidsCookie + +class HttpUtil { + + public static final String UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + + public static final String PG_TRX_ID_HEADER = "pg-trx-id" + public static final String AUTHORIZATION_HEADER = "Authorization" + public static final String ACCEPT_HEADER = "Authorization" + public static final String CONTENT_TYPE_HEADER = "Content-Type" + public static final String COOKIE_HEADER = "cookie" + + public static final String CONTENT_TYPE_HEADER_VALUE = "application/json" + public static final String CHARSET_HEADER_VALUE = "charset=utf-8" + + static String makeBasicAuthHeaderValue(String username, String password) { + "Basic ${encode("$username:$password")}" + } + + static HashMap getCookieHeader(ObjectMapperWrapper mapper, UidsCookie uidsCookie) { + [(COOKIE_HEADER): makeUidsCookieHeaderValue(mapper.encode(uidsCookie))] + } + + private static String makeUidsCookieHeaderValue(String uidsCookieJson) { + "uids=${encode(uidsCookieJson)}" + } + + private static encode(String string) { + Base64.encoder.encodeToString(string.bytes) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy index bf44c768156..4016cf7ad73 100644 --- a/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/ObjectMapperWrapper.groovy @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +// TODO make this into a Trait so that we won't need to pass it around. This will allow us to use it in the models class ObjectMapperWrapper { private final ObjectMapper mapper diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index 1dccc13f1a0..c0cb1a87119 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -11,6 +11,8 @@ import java.util.stream.IntStream import static java.lang.Integer.MAX_VALUE import static java.lang.Integer.MIN_VALUE +import static java.util.concurrent.TimeUnit.MILLISECONDS +import static org.awaitility.Awaitility.with class PBSUtils { @@ -54,4 +56,12 @@ class PBSUtils { static String getPropertyOrDefault(String property, String defaultValue) { System.getProperty(property) ?: defaultValue } + + static void waitUntil(Closure closure, long timeout = 1000, long pollInterval = 100) { + with().pollDelay(0, MILLISECONDS) + .pollInterval(pollInterval, MILLISECONDS) + .await() + .atMost(timeout, MILLISECONDS) + .until(closure) + } } diff --git a/src/test/java/org/prebid/server/analytics/AnalyticsReporterDelegatorTest.java b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java similarity index 99% rename from src/test/java/org/prebid/server/analytics/AnalyticsReporterDelegatorTest.java rename to src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java index b5a829745e2..6e7647159dd 100644 --- a/src/test/java/org/prebid/server/analytics/AnalyticsReporterDelegatorTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics; +package org.prebid.server.analytics.reporter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.IntNode; @@ -18,6 +18,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; +import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.auction.model.AuctionContext; diff --git a/src/test/java/org/prebid/server/analytics/LogAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporterTest.java similarity index 94% rename from src/test/java/org/prebid/server/analytics/LogAnalyticsReporterTest.java rename to src/test/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporterTest.java index 66900b297de..e988327da30 100644 --- a/src/test/java/org/prebid/server/analytics/LogAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/log/LogAnalyticsReporterTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics; +package org.prebid.server.analytics.reporter.log; import org.junit.Test; import org.prebid.server.VertxTest; diff --git a/src/test/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporterTest.java similarity index 97% rename from src/test/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporterTest.java rename to src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporterTest.java index 8709a688d80..d12201b0df4 100644 --- a/src/test/java/org/prebid/server/analytics/pubstack/PubstackAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackAnalyticsReporterTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics.pubstack; +package org.prebid.server.analytics.reporter.pubstack; import com.fasterxml.jackson.core.JsonProcessingException; import io.vertx.core.Future; @@ -15,9 +15,9 @@ import org.prebid.server.analytics.model.CookieSyncEvent; import org.prebid.server.analytics.model.SetuidEvent; import org.prebid.server.analytics.model.VideoEvent; -import org.prebid.server.analytics.pubstack.model.EventType; -import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties; -import org.prebid.server.analytics.pubstack.model.PubstackConfig; +import org.prebid.server.analytics.reporter.pubstack.model.EventType; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackConfig; import org.prebid.server.exception.PreBidException; import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; diff --git a/src/test/java/org/prebid/server/analytics/pubstack/PubstackEventHandlerTest.java b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java similarity index 98% rename from src/test/java/org/prebid/server/analytics/pubstack/PubstackEventHandlerTest.java rename to src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java index bf244a529a4..acc7a1cd113 100644 --- a/src/test/java/org/prebid/server/analytics/pubstack/PubstackEventHandlerTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.analytics.pubstack; +package org.prebid.server.analytics.reporter.pubstack; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -13,7 +13,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.model.SetuidEvent; -import org.prebid.server.analytics.pubstack.model.PubstackAnalyticsProperties; +import org.prebid.server.analytics.reporter.pubstack.model.PubstackAnalyticsProperties; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.deals.model.DeepDebugLog; diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index f106bb11376..ddc4d36d487 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -1173,6 +1173,100 @@ public void makeHttpRequestsShouldNotCreateUserIfVisitorAndConsentNotPresent() { .containsOnly((User) null); } + @Test + public void makeHttpRequestsShouldUseUserBuyeruidIfPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.user(User.builder() + .buyeruid("buyeruid") + .ext(ExtUser.builder() + .eids(singletonList(ExtUserEid.of( + "rubiconproject.com", + null, + singletonList(ExtUserEidUid.of( + "extUserEidUidId", + null, + ExtUserEidUidExt.of(null, "ppuid") + )), + null))) + .build()) + .build()), + builder -> builder.video(Video.builder().build()), identity()); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getUser) + .extracting(User::getBuyeruid) + .containsExactly("buyeruid"); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseUidIdIfUserBuyeruidAbsentAndSpecialEidSourceAndStypeIsPpuid() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.user(User.builder() + .ext(ExtUser.builder() + .eids(singletonList(ExtUserEid.of( + "rubiconproject.com", + null, + singletonList(ExtUserEidUid.of( + "extUserEidUidId", + null, + ExtUserEidUidExt.of(null, "ppuid") + )), + null))) + .build()) + .build()), + builder -> builder.video(Video.builder().build()), identity()); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getUser) + .extracting(User::getBuyeruid) + .containsExactly("extUserEidUidId"); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseUidIdIfUserBuyeruidAbsentAndSpecialEidSourceAndStypeIsOther() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder.user(User.builder() + .ext(ExtUser.builder() + .eids(singletonList(ExtUserEid.of( + "rubiconproject.com", + null, + singletonList(ExtUserEidUid.of( + "extUserEidUidId", + null, + ExtUserEidUidExt.of(null, "other") + )), + null))) + .build()) + .build()), + builder -> builder.video(Video.builder().build()), identity()); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getUser) + .extracting(User::getBuyeruid) + .containsExactly("extUserEidUidId"); + assertThat(result.getErrors()).isEmpty(); + } + @Test public void makeHttpRequestsShouldCreateUserExtTpIdWithAdServerEidSource() { // given diff --git a/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java b/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java index a0d4657af67..e0911a31fb1 100644 --- a/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/tappx/TappxBidderTest.java @@ -23,6 +23,7 @@ import org.prebid.server.proto.openrtb.ext.request.tappx.ExtImpTappx; import java.math.BigDecimal; +import java.time.Clock; import java.util.List; import java.util.function.Function; @@ -34,13 +35,13 @@ public class TappxBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://"; + private static final String ENDPOINT_URL = "https://{{subdomain}}.domain"; private TappxBidder tappxBidder; @Before public void setUp() { - tappxBidder = new TappxBidder(ENDPOINT_URL, jacksonMapper); + tappxBidder = new TappxBidder(ENDPOINT_URL, Clock.systemDefaultZone(), jacksonMapper); } @Test @@ -61,29 +62,6 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { assertThat(result.getValue()).isEmpty(); } - @Test - public void makeHttpRequestsShouldReturnErrorIfEndpointUrlComposingFails() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("invalid host", "tappxkey", "endpoint", null, - null, null, null)))) - .build())) - .build(); - - // when - final Result>> result = tappxBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getMessage()) - .startsWith("Failed to build endpoint URL: Illegal character in authority at index 8"); - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); - }); - } - @Test public void makeHttpRequestsShouldNotModifyIncomingRequestImp() { // given @@ -99,8 +77,12 @@ public void makeHttpRequestsShouldNotModifyIncomingRequestImp() { // then final ExtRequest extRequest = ExtRequest.empty(); - final TappxBidderExt tappxBidderExt = TappxBidderExt.of("tappxkey", "mktag", singletonList("bcid"), - singletonList("bcrid")); + final TappxBidderExt tappxBidderExt = TappxBidderExt.builder() + .tappxkey("tappxkey") + .mktag("mktag") + .bcid(singletonList("bcid")) + .bcrid(singletonList("bcrid")) + .build(); extRequest.addProperty("bidder", mapper.valueToTree(tappxBidderExt)); assertThat(result.getErrors()).isEmpty(); @@ -119,8 +101,12 @@ public void makeHttpRequestsShouldModifyBidRequestExt() { // then final ExtRequest extRequest = ExtRequest.empty(); - final TappxBidderExt tappxBidderExt = TappxBidderExt.of("tappxkey", "mktag", singletonList("bcid"), - singletonList("bcrid")); + final TappxBidderExt tappxBidderExt = TappxBidderExt.builder() + .tappxkey("tappxkey") + .mktag("mktag") + .bcid(singletonList("bcid")) + .bcrid(singletonList("bcrid")) + .build(); extRequest.addProperty("bidder", mapper.valueToTree(tappxBidderExt)); assertThat(result.getErrors()).isEmpty(); @@ -161,31 +147,7 @@ public void makeHttpRequestsShouldMakeRequestWithUrl() { // then assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "https://host/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; - assertThat(result.getValue()).hasSize(1) - .allSatisfy(httpRequest -> { - assertThat(httpRequest.getUri()).isEqualTo(expectedUri); - assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); - }); - } - - @Test - public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameter() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host/rtb/v2/", "tappxkey", "endpoint", BigDecimal.ONE, - null, null, null)))) - .build())) - .build(); - - // when - final Result>> result = tappxBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "https://host/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.4&type_cnn=prebid"; assertThat(result.getValue()).hasSize(1) .allSatisfy(httpRequest -> { assertThat(httpRequest.getUri()).isEqualTo(expectedUri); @@ -199,7 +161,7 @@ public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterButWithou final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host/rtb/v2", "tappxkey", "endpoint", BigDecimal.ONE, + ExtImpTappx.of(null, "tappxkey", "endpoint", BigDecimal.ONE, null, null, null)))) .build())) .build(); @@ -209,7 +171,7 @@ public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterButWithou // then assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "https://host/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.4&type_cnn=prebid"; assertThat(result.getValue()).hasSize(1) .allSatisfy(httpRequest -> { assertThat(httpRequest.getUri()).isEqualTo(expectedUri); @@ -218,12 +180,12 @@ public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterButWithou } @Test - public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterAndSlashBeforeEndpoint() { + public void makeHttpRequestShouldBuildCorrectUriWithEndPointParameterIfMatched() { // given final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host/rtb/v2", "tappxkey", "/endpoint", BigDecimal.ONE, + ExtImpTappx.of(null, "tappxkey", "zz855226test", BigDecimal.ONE, null, null, null)))) .build())) .build(); @@ -233,31 +195,8 @@ public void makeHttpRequestShouldBuildCorrectUriWithPathInHostParameterAndSlashB // then assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "https://host/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; - assertThat(result.getValue()).hasSize(1) - .allSatisfy(httpRequest -> { - assertThat(httpRequest.getUri()).isEqualTo(expectedUri); - assertThat(httpRequest.getMethod()).isEqualTo(HttpMethod.POST); - }); - } - - @Test - public void makeHttpRequestShouldBuildCorrectUriWithWeirdCaseHttpsSchemeInHostParam() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("htTpS://host-host.com/rtb/v2", "tappxkey", "/endpoint", - BigDecimal.ONE, null, null, null)))) - .build())) - .build(); - - // when - final Result>> result = tappxBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "htTpS://host-host.com/rtb/v2/endpoint?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + final String expectedUri = + "https://zz855226test.pub.domain/rtb/?tappxkey=tappxkey&v=1.4&type_cnn=prebid"; assertThat(result.getValue()).hasSize(1) .allSatisfy(httpRequest -> { assertThat(httpRequest.getUri()).isEqualTo(expectedUri); @@ -271,7 +210,7 @@ public void makeHttpRequestsShouldModifyUrl() { final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("endpoint.host", "tappxkey", "endpoint", BigDecimal.ONE, + ExtImpTappx.of(null, "tappxkey", "endpoint", BigDecimal.ONE, null, null, null)))) .build())) .build(); @@ -281,7 +220,7 @@ public void makeHttpRequestsShouldModifyUrl() { // then assertThat(result.getErrors()).isEmpty(); - final String expectedUri = "https://endpoint.host?tappxkey=tappxkey&v=1.3&type_cnn=prebid"; + final String expectedUri = "https://ssp.api.domain/rtb/v2/endpoint?tappxkey=tappxkey&v=1.4&type_cnn=prebid"; assertThat(result.getValue()).hasSize(1) .allSatisfy(httpRequest -> { assertThat(httpRequest.getUri()).isEqualTo(expectedUri); @@ -289,46 +228,6 @@ public void makeHttpRequestsShouldModifyUrl() { }); } - @Test - public void makeHttpRequestsShouldReturnErrorWhenEitherOfExtParametersIsEmpty() { - // given - final BidRequest bidRequestEmptyHost = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("", "tappxkey", "endpoint", BigDecimal.ONE, - null, null, null)))).build())) - .build(); - - final BidRequest bidRequestEmptyTappxKey = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host", "", "endpoint", BigDecimal.ONE, - null, null, null)))).build())) - .build(); - - final BidRequest bidRequestEmptyEndpoint = BidRequest.builder() - .imp(singletonList(Imp.builder() - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpTappx.of("host", "tappxkey", "", BigDecimal.ONE, - null, null, null)))).build())) - .build(); - - // when - final Result>> emptyHostResult = tappxBidder.makeHttpRequests(bidRequestEmptyHost); - final Result>> emptyTappxKeyResult = - tappxBidder.makeHttpRequests(bidRequestEmptyTappxKey); - final Result>> emptyEndpointResult = - tappxBidder.makeHttpRequests(bidRequestEmptyEndpoint); - - // then - assertThat(emptyHostResult.getErrors()).hasSize(1); - assertThat(emptyHostResult.getErrors().get(0).getMessage()).startsWith("Tappx host undefined"); - assertThat(emptyTappxKeyResult.getErrors()).hasSize(1); - assertThat(emptyTappxKeyResult.getErrors().get(0).getMessage()).startsWith("Tappx tappxkey undefined"); - assertThat(emptyEndpointResult.getErrors()).hasSize(1); - assertThat(emptyEndpointResult.getErrors().get(0).getMessage()).startsWith("Tappx endpoint undefined"); - } - @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index 85d9fbd39d4..ffd69d5c330 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -15,8 +15,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.CookieSyncEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; @@ -76,8 +76,8 @@ import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class CookieSyncHandlerTest extends VertxTest { diff --git a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java index 52d08c3c61e..cd7e2399e7e 100644 --- a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java @@ -15,8 +15,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.model.Tuple2; import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.deals.UserService; diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 4144e2d7a22..d216ecb635f 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -15,8 +15,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.SetuidEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.PrivacyEnforcementService; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.Usersyncer; diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index acc72b487ad..1c6cf2ccc21 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -22,8 +22,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index c6b65078c54..451a15c1064 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -18,8 +18,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; diff --git a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java index 053dc6cb0e0..410093b9b5a 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java @@ -16,8 +16,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; -import org.prebid.server.analytics.AnalyticsReporterDelegator; import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.model.AuctionContext; diff --git a/src/test/java/org/prebid/server/it/TappxTest.java b/src/test/java/org/prebid/server/it/TappxTest.java index 490ab0f3e4c..8e78d62ce53 100644 --- a/src/test/java/org/prebid/server/it/TappxTest.java +++ b/src/test/java/org/prebid/server/it/TappxTest.java @@ -21,7 +21,7 @@ public class TappxTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromTappx() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/tappx-exchange")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/tappx-exchange/rtb/v2/test")) .withRequestBody(equalToJson(jsonFrom("openrtb2/tappx/test-tappx-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/tappx/test-tappx-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json index e4c871ea24e..ab61d636585 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-request.json @@ -10,8 +10,7 @@ "ext": { "tappx": { "tappxkey": "pub-12345-android-9876", - "endpoint": "tappx-exchange", - "host": "localhost:8090", + "endpoint": "test", "bidfloor": 1.5 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json index fad771082b3..6594f31a9b9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-tappx-bid-request.json @@ -11,8 +11,7 @@ "ext": { "bidder": { "tappxkey": "pub-12345-android-9876", - "endpoint": "tappx-exchange", - "host": "localhost:8090", + "endpoint": "test", "bidfloor": 1.5 } } @@ -47,4 +46,4 @@ "tappxkey": "pub-12345-android-9876" } } -} \ No newline at end of file +} 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 b05d11ec9e6..f78d2333324 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -232,7 +232,7 @@ adapters.sharethrough.endpoint=http://localhost:8090/sharethrough-exchange adapters.silvermob.enabled=true adapters.silvermob.endpoint=http://localhost:8090/silvermob-exchange adapters.tappx.enabled=true -adapters.tappx.endpoint=http:// +adapters.tappx.endpoint=http://localhost:8090/tappx-exchange adapters.telaria.enabled=true adapters.telaria.endpoint=http://localhost:8090/telaria-exchange/ adapters.triplelift.enabled=true