diff --git a/docs/application-settings.md b/docs/application-settings.md index 7a4a72f38d1..bf89c1c3d83 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -12,6 +12,7 @@ There are two ways to configure application settings: database and file. This do - `auction.truncate-target-attr` - Maximum targeting attributes size. Values between 1 and 255. - `auction.default-integration` - Default integration to assume. - `auction.debug-allow` - enables debug output in the auction response. Default `true`. +- `auction.impression-limit` - a max number of impressions allowed for the auction, impressions that exceed this limit will be dropped, 0 means no limit. - `auction.bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. Valid values are: - "skip": don't do anything about creative max size for this publisher diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 6d2ac3ee72d..b8fcd6d6301 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -414,6 +414,10 @@ private Future updateBidRequest(AuctionContext auctionContext) { .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())) + .compose(resolvedBidRequest -> ortb2RequestFactory.limitImpressions( + account, + resolvedBidRequest, + auctionContext.getDebugWarnings())) .compose(resolvedBidRequest -> ortb2RequestFactory.validateRequest( account, resolvedBidRequest, diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 628ea212fd8..13df5f4df82 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -241,6 +241,7 @@ private Future updateAndValidateBidRequest(AuctionContext auctionCon return storedRequestProcessor.processAuctionRequest(account.getId(), auctionContext.getBidRequest()) .compose(auctionStoredResult -> updateBidRequest(auctionStoredResult, auctionContext)) + .compose(bidRequest -> ortb2RequestFactory.limitImpressions(account, bidRequest, debugWarnings)) .compose(bidRequest -> ortb2RequestFactory.validateRequest( account, bidRequest, httpRequest, auctionContext.getDebugContext(), debugWarnings)) .map(interstitialProcessor::process); diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 5a122d5a64e..d50b35d3717 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -6,6 +6,7 @@ import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; @@ -192,6 +193,23 @@ public Future activityInfrastructureFrom(AuctionContext auctionContext.getDebugContext().getTraceLevel())); } + public Future limitImpressions(Account account, BidRequest bidRequest, List warnings) { + final List imps = bidRequest.getImp(); + final int impsLimit = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getImpressionLimit) + .orElse(0); + + if (impsLimit > 0 && imps.size() > impsLimit) { + metrics.updateImpsDroppedMetric(imps.size() - impsLimit); + warnings.add(("Only first %d impressions were kept due to the limit, " + + "all the subsequent impressions have been dropped for the auction").formatted(impsLimit)); + return Future.succeededFuture(bidRequest.toBuilder().imp(imps.subList(0, impsLimit)).build()); + } + + return Future.succeededFuture(bidRequest); + } + public Future validateRequest(Account account, BidRequest bidRequest, HttpRequestContext httpRequestContext, diff --git a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java index 2e41fd97a35..811db804fb9 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java @@ -119,6 +119,12 @@ public Future> fromRequest(RoutingContext routingC .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) + .compose(auctionContext -> ortb2RequestFactory.limitImpressions( + auctionContext.getAccount(), + auctionContext.getBidRequest(), + auctionContext.getDebugWarnings()) + .map(auctionContext::with)) + .compose(auctionContext -> ortb2RequestFactory.validateRequest( auctionContext.getAccount(), auctionContext.getBidRequest(), diff --git a/src/main/java/org/prebid/server/bidder/Usersyncer.java b/src/main/java/org/prebid/server/bidder/Usersyncer.java index e5b6cad6f6e..ebe776d2e3e 100644 --- a/src/main/java/org/prebid/server/bidder/Usersyncer.java +++ b/src/main/java/org/prebid/server/bidder/Usersyncer.java @@ -3,6 +3,8 @@ import lombok.Value; import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; +import java.util.List; + @Value(staticConstructor = "of") public class Usersyncer { @@ -16,7 +18,23 @@ public class Usersyncer { UsersyncMethod redirect; - public static Usersyncer of(String cookieFamilyName, UsersyncMethod iframe, UsersyncMethod redirect) { - return of(true, cookieFamilyName, CookieFamilySource.ROOT, iframe, redirect); + boolean skipWhenInGdprScope; + + List gppSidToSkip; + + public static Usersyncer of(String cookieFamilyName, + UsersyncMethod iframe, + UsersyncMethod redirect, + boolean skipWhenInGdprScope, + List gppSidToSkip) { + + return of( + true, + cookieFamilyName, + CookieFamilySource.ROOT, + iframe, + redirect, + skipWhenInGdprScope, + gppSidToSkip); } } diff --git a/src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java b/src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java new file mode 100644 index 00000000000..c214112735e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java @@ -0,0 +1,180 @@ +package org.prebid.server.bidder.akcelo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.proto.openrtb.ext.request.akcelo.ExtImpAkcelo; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class AkceloBidder implements Bidder { + + private static final TypeReference> AKCELO_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String BIDDER_NAME = "akcelo"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AkceloBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List imps = request.getImp(); + final List modifiedImps = new ArrayList<>(); + + final ExtImpAkcelo firstExtImp; + try { + firstExtImp = parseImpExt(imps.getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + for (final Imp imp : imps) { + modifiedImps.add(modifyImp(imp)); + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, firstExtImp.getSiteId()); + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } + + private ExtImpAkcelo parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), AKCELO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp) { + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set(BIDDER_NAME, imp.getExt().get("bidder"))) + .build(); + } + + private BidRequest modifyRequest(BidRequest request, List imps, String siteId) { + return request.toBuilder() + .imp(imps) + .site(modifySite(request.getSite(), siteId)) + .build(); + } + + private Site modifySite(Site site, String siteId) { + final Publisher publisher = Optional.ofNullable(site) + .map(Site::getPublisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .ext(ExtPublisher.of(ExtPublisherPrebid.of(siteId))) + .build(); + + return Optional.ofNullable(site) + .map(Site::toBuilder) + .orElseGet(Site::builder) + .publisher(publisher) + .build(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType == null ? null : BidderBid.of(bid, bidType, currency); + } + + private BidType getBidType(Bid bid, List errors) { + final Integer mType = bid.getMtype(); + if (mType != null) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> { + errors.add(BidderError.badServerResponse("unable to get media type " + mType)); + yield null; + } + }; + } + + return getExtBidPrebidType(bid, errors); + } + + private BidType getExtBidPrebidType(Bid bid, List errors) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get("prebid")) + .filter(JsonNode::isObject) + .map(ObjectNode.class::cast) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseGet(() -> { + errors.add(BidderError.badServerResponse("missing media type for bid " + bid.getId())); + return null; + }); + } + + private ExtBidPrebid parseExtBidPrebid(ObjectNode prebid) { + try { + return mapper.mapper().treeToValue(prebid, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/MediasquareBidder.java b/src/main/java/org/prebid/server/bidder/mediasquare/MediasquareBidder.java new file mode 100644 index 00000000000..18c9fca362b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/MediasquareBidder.java @@ -0,0 +1,296 @@ +package org.prebid.server.bidder.mediasquare; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.mediasquare.request.MediasquareBanner; +import org.prebid.server.bidder.mediasquare.request.MediasquareCode; +import org.prebid.server.bidder.mediasquare.request.MediasquareFloor; +import org.prebid.server.bidder.mediasquare.request.MediasquareGdpr; +import org.prebid.server.bidder.mediasquare.request.MediasquareMediaTypes; +import org.prebid.server.bidder.mediasquare.request.MediasquareRequest; +import org.prebid.server.bidder.mediasquare.request.MediasquareSupport; +import org.prebid.server.bidder.mediasquare.response.MediasquareBid; +import org.prebid.server.bidder.mediasquare.response.MediasquareResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.mediasquare.ExtImpMediasquare; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class MediasquareBidder implements Bidder { + + private static final String SIZE_FORMAT = "%dx%d"; + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MediasquareBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List codes = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMediasquare extImp = parseImpExt(imp); + final MediasquareCode mediasquareCode = makeCode(request, imp, extImp); + if (isCodeValid(mediasquareCode)) { + codes.add(mediasquareCode); + } + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (codes.isEmpty()) { + return Result.withErrors(errors); + } + + final MediasquareRequest outgoingRequest = makeRequest(request, codes); + + final HttpRequest httpRequest = HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .body(mapper.encodeToBytes(outgoingRequest)) + .payload(outgoingRequest) + .impIds(BidderUtil.impIds(request)) + .build(); + + return Result.of(List.of(httpRequest), errors); + } + + private ExtImpMediasquare parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("can not parse imp.ext" + e.getMessage()); + } + } + + private static MediasquareCode makeCode(BidRequest bidRequest, Imp imp, ExtImpMediasquare extImp) { + final MediasquareMediaTypes mediaTypes = makeMediaTypes(imp); + final Map floors = mediaTypes == null + ? null + : makeFloors(MediasquareFloor.of(imp.getBidfloor(), imp.getBidfloorcur()), mediaTypes); + + return MediasquareCode.builder() + .adUnit(imp.getTagid()) + .auctionId(bidRequest.getId()) + .bidId(imp.getId()) + .code(extImp.getCode()) + .owner(extImp.getOwner()) + .mediaTypes(mediaTypes) + .floor(floors) + .build(); + } + + private static MediasquareMediaTypes makeMediaTypes(Imp imp) { + final Video video = imp.getVideo(); + final Banner banner = imp.getBanner(); + final Native xNative = imp.getXNative(); + + if (video == null && banner == null && xNative == null) { + return null; + } + + return MediasquareMediaTypes.builder() + .banner(makeBanner(banner)) + .video(video) + .nativeRequest(xNative != null ? xNative.getRequest() : null) + .build(); + } + + private static MediasquareBanner makeBanner(Banner banner) { + if (banner == null) { + return null; + } + + final List> sizes = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(banner.getFormat())) { + for (Format format : banner.getFormat()) { + sizes.add(List.of(format.getW(), format.getH())); + } + } else { + sizes.add(List.of(banner.getW(), banner.getH())); + } + + return MediasquareBanner.of(sizes); + } + + private static Map makeFloors(MediasquareFloor floor, MediasquareMediaTypes mediaTypes) { + final Map floors = new HashMap<>(); + + final Video video = mediaTypes.getVideo(); + final MediasquareBanner banner = mediaTypes.getBanner(); + final String xNative = mediaTypes.getNativeRequest(); + + if (video != null) { + if (video.getW() != null && video.getH() != null) { + final String videoSize = SIZE_FORMAT.formatted(video.getW(), video.getH()); + floors.put(videoSize, floor); + } + floors.put("*", floor); + } + + if (banner != null) { + for (List format: banner.getSizes()) { + floors.put(SIZE_FORMAT.formatted(format.get(0), format.get(1)), floor); + } + } + + if (xNative != null) { + floors.put("*", floor); + } + + return MapUtils.isNotEmpty(floors) ? floors : null; + } + + private static boolean isCodeValid(MediasquareCode code) { + final MediasquareMediaTypes mediaTypes = code.getMediaTypes(); + return mediaTypes != null && ObjectUtils.anyNotNull( + mediaTypes.getBanner(), mediaTypes.getVideo(), mediaTypes.getNativeRequest()); + } + + private MediasquareRequest makeRequest(BidRequest bidRequest, List codes) { + final User user = bidRequest.getUser(); + final Regs regs = bidRequest.getRegs(); + + return MediasquareRequest.builder() + .codes(codes) + .dsa(getDsa(regs)) + .gdpr(makeGdpr(user, regs)) + .type("pbs") + .support(MediasquareSupport.of(bidRequest.getDevice(), bidRequest.getApp())) + .test(Objects.equals(bidRequest.getTest(), 1)) + .build(); + } + + private static ExtRegsDsa getDsa(Regs regs) { + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .orElse(null); + } + + private static MediasquareGdpr makeGdpr(User user, Regs regs) { + final boolean gdprApplies = Optional.ofNullable(regs) + .map(Regs::getGdpr) + .map(gdpr -> gdpr == 1) + .orElse(false); + final String consent = user != null ? user.getConsent() : null; + return MediasquareGdpr.of(gdprApplies, consent); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final MediasquareResponse response = mapper.decodeValue( + httpCall.getResponse().getBody(), + MediasquareResponse.class); + return Result.withValues(extractBids(response)); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse("Failed to decode response: " + e.getMessage())); + } + } + + private List extractBids(MediasquareResponse response) { + if (response == null || CollectionUtils.isEmpty(response.getResponses())) { + return Collections.emptyList(); + } + + return response.getResponses().stream() + .filter(Objects::nonNull) + .map(this::makeBidderBid) + .collect(Collectors.toList()); + } + + private BidderBid makeBidderBid(MediasquareBid bid) { + final BidType bidType = getBidType(bid); + return BidderBid.of(makeBid(bid, bidType), bidType, bid.getCurrency()); + } + + private static BidType getBidType(MediasquareBid bid) { + if (bid.getVideo() != null) { + return BidType.video; + } + if (bid.getNativeResponse() != null) { + return BidType.xNative; + } + return BidType.banner; + } + + private Bid makeBid(MediasquareBid bid, BidType bidType) { + return Bid.builder() + .id(bid.getId()) + .impid(bid.getBidId()) + .price(bid.getCpm()) + .adm(bid.getAd()) + .adomain(bid.getAdomain()) + .w(bid.getWidth()) + .h(bid.getHeight()) + .crid(bid.getCreativeId()) + .mtype(bidType.ordinal() + 1) + .burl(bid.getBurl()) + .ext(getBidExt(bid, bidType)) + .build(); + } + + private ObjectNode getBidExt(MediasquareBid bid, BidType bidType) { + final ExtBidPrebidMeta meta = ExtBidPrebidMeta.builder() + .advertiserDomains(bid.getAdomain() != null ? bid.getAdomain() : null) + .mediaType(bidType.getName()) + .build(); + + final ExtBidPrebid prebid = ExtBidPrebid.builder().meta(meta).build(); + + final ObjectNode bidExt = mapper.mapper().createObjectNode(); + if (bid.getDsa() != null) { + bidExt.set("dsa", bid.getDsa()); + } + bidExt.set("prebid", mapper.mapper().valueToTree(prebid)); + + return bidExt; + } +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareBanner.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareBanner.java new file mode 100644 index 00000000000..bb8af052d81 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareBanner.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.mediasquare.request; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class MediasquareBanner { + + List> sizes; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareCode.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareCode.java new file mode 100644 index 00000000000..ea7559dcab8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareCode.java @@ -0,0 +1,30 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value(staticConstructor = "of") +public class MediasquareCode { + + @JsonProperty("adunit") + String adUnit; + + @JsonProperty("auctionid") + String auctionId; + + @JsonProperty("bidid") + String bidId; + + String code; + + String owner; + + @JsonProperty("mediatypes") + MediasquareMediaTypes mediaTypes; + + Map floor; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareFloor.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareFloor.java new file mode 100644 index 00000000000..18987f20f2a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareFloor.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.mediasquare.request; + +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class MediasquareFloor { + + BigDecimal floor; + + String currency; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareGdpr.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareGdpr.java new file mode 100644 index 00000000000..6dcf676a2db --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareGdpr.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.mediasquare.request; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediasquareGdpr { + + boolean consentRequired; + + String consentString; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareMediaTypes.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareMediaTypes.java new file mode 100644 index 00000000000..da8e5ec59a7 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareMediaTypes.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.iab.openrtb.request.Video; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value(staticConstructor = "of") +public class MediasquareMediaTypes { + + MediasquareBanner banner; + + Video video; + + String nativeRequest; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareRequest.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareRequest.java new file mode 100644 index 00000000000..d236e55b80f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareRequest.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; + +import java.util.List; + +@Builder +@Value(staticConstructor = "of") +public class MediasquareRequest { + + List codes; + + MediasquareGdpr gdpr; + + String type; + + ExtRegsDsa dsa; + + @JsonProperty("tech") + MediasquareSupport support; + + Boolean test; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareSupport.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareSupport.java new file mode 100644 index 00000000000..33df240a732 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareSupport.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediasquareSupport { + + Device device; + + App app; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareBid.java b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareBid.java new file mode 100644 index 00000000000..57301c8df2c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareBid.java @@ -0,0 +1,49 @@ +package org.prebid.server.bidder.mediasquare.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; +import java.util.List; + +@Value +@Builder(toBuilder = true) +public class MediasquareBid { + + String id; + + String ad; + + String bidId; + + String bidder; + + BigDecimal cpm; + + String currency; + + String creativeId; + + Integer height; + + Integer width; + + Boolean netRevenue; + + String transactionId; + + Integer ttl; + + ObjectNode video; + + @JsonProperty("native") + ObjectNode nativeResponse; + + List adomain; + + ObjectNode dsa; + + String burl; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareResponse.java b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareResponse.java new file mode 100644 index 00000000000..8260fb633ee --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.mediasquare.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class MediasquareResponse { + + List responses; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java index 68c0d862681..2f9722b2c82 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -1,13 +1,11 @@ package org.prebid.server.bidder.missena; import com.fasterxml.jackson.annotation.JsonProperty; -import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.BidRequest; import lombok.Builder; import lombok.Value; import java.math.BigDecimal; -import java.util.List; @Value @Builder(toBuilder = true) @@ -16,44 +14,23 @@ public class MissenaAdRequest { @JsonProperty("adunit") String adUnit; - @JsonProperty("buyeruid") - String buyerUid; - - Integer coppa; - String currency; - @JsonProperty("userEids") - List userEids; - BigDecimal floor; String floorCurrency; - @JsonProperty("consent_required") - Boolean gdpr; - - @JsonProperty("consent_string") - String gdprConsent; - @JsonProperty("ik") String idempotencyKey; - String referer; - - String refererCanonical; - String requestId; - SupplyChain schain; - Long timeout; - String url; - MissenaUserParams params; - String usPrivacy; + @JsonProperty("ortb2") + BidRequest bidRequest; String version; } diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java index 4cad9af88f7..eacc3a49541 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -4,10 +4,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Source; -import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; @@ -24,8 +21,6 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtRegs; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; @@ -39,7 +34,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Optional; public class MissenaBidder implements Bidder { @@ -91,10 +85,7 @@ private ExtImpMissena parseImpExt(Imp imp) { private HttpRequest makeHttpRequest(BidRequest request, Imp imp, ExtImpMissena extImp) { final Site site = request.getSite(); - final User user = request.getUser(); - final Regs regs = request.getRegs(); final Device device = request.getDevice(); - final Source source = request.getSource(); final String requestCurrency = resolveCurrency(request.getCur()); final Price floorInfo = resolveBidFloor(imp, request, requestCurrency); @@ -108,22 +99,15 @@ private HttpRequest makeHttpRequest(BidRequest request, Imp im final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() .adUnit(imp.getId()) - .buyerUid(user != null ? user.getBuyeruid() : null) - .coppa(regs != null ? regs.getCoppa() : null) .currency(requestCurrency) - .userEids(user != null ? user.getEids() : null) .floor(floorInfo.getValue()) .floorCurrency(floorInfo.getCurrency()) - .gdpr(isGdpr(regs)) - .gdprConsent(getUserConsent(user)) .idempotencyKey(request.getId()) - .referer(site != null ? site.getPage() : null) - .refererCanonical(site != null ? site.getDomain() : null) .requestId(request.getId()) - .schain(source != null ? source.getSchain() : null) .timeout(request.getTmax()) .params(userParams) .version(prebidVersionProvider.getNameVersionRecord()) + .bidRequest(request) .build(); return HttpRequest.builder() @@ -199,22 +183,6 @@ private String resolveEndpointUrl(String apiKey) { return endpointUrl.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(apiKey)); } - private static boolean isGdpr(Regs regs) { - return Optional.ofNullable(regs) - .map(Regs::getExt) - .map(ExtRegs::getGdpr) - .map(gdpr -> gdpr == 1) - .orElse(false); - } - - private static String getUserConsent(User user) { - return Optional.ofNullable(user) - .map(User::getExt) - .map(ExtUser::getConsent) - .filter(StringUtils::isNotBlank) - .orElse(null); - } - @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java new file mode 100644 index 00000000000..180f545bf47 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.nexx360; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.nexx360.ExtImpNexx360; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Nexx360Bidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String BIDDER_NAME = "nexx360"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final PrebidVersionProvider prebidVersionProvider; + + public Nexx360Bidder(String endpointUrl, JacksonMapper mapper, PrebidVersionProvider prebidVersionProvider) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List imps = request.getImp(); + final List modifiedImps = new ArrayList<>(); + + final ExtImpNexx360 firstExtImp; + try { + firstExtImp = parseImpExt(imps.getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + for (final Imp imp : imps) { + modifiedImps.add(modifyImp(imp)); + } + + final BidRequest modifiedRequest = makeRequest(request, modifiedImps); + final String url = makeUrl(firstExtImp.getTagId(), firstExtImp.getPlacement()); + return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, url, mapper)); + } + + private ExtImpNexx360 parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp) { + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set(BIDDER_NAME, imp.getExt().get("bidder"))) + .build(); + } + + private BidRequest makeRequest(BidRequest request, List imps) { + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty(BIDDER_NAME, mapper.mapper().valueToTree( + Nexx360ExtRequest.of(Nexx360ExtRequestCaller.of(prebidVersionProvider.getNameVersionRecord())))); + + return request.toBuilder() + .imp(imps) + .ext(extRequest) + .build(); + } + + private String makeUrl(String tagId, String placement) { + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException("Invalid url: %s, error: %s".formatted(endpointUrl, e.getMessage())); + } + + if (StringUtils.isNotBlank(placement)) { + uriBuilder.addParameter("placement", placement); + } + if (StringUtils.isNotBlank(tagId)) { + uriBuilder.addParameter("tag_id", tagId); + } + + return uriBuilder.toString(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) { + final String bidType; + try { + bidType = mapper.mapper() + .convertValue(bid.getExt(), Nexx360ExtBid.class) + .getBidType(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "unable to fetch mediaType in multi-format: " + bid.getImpid()); + } + + return switch (bidType) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + case "audio" -> BidType.audio; + case "native" -> BidType.xNative; + default -> throw new PreBidException( + "unable to fetch mediaType in multi-format: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java new file mode 100644 index 00000000000..3e4ecaf4cda --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.nexx360; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Nexx360ExtBid { + + @JsonProperty("bidType") + String bidType; +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java new file mode 100644 index 00000000000..1ff6bc3ead1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.nexx360; + +import lombok.Value; + +import java.util.Collections; +import java.util.List; + +@Value(staticConstructor = "of") +public class Nexx360ExtRequest { + + List caller; + + public static Nexx360ExtRequest of(Nexx360ExtRequestCaller caller) { + return of(Collections.singletonList(caller)); + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java new file mode 100644 index 00000000000..72da1af310a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.nexx360; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Nexx360ExtRequestCaller { + + String name; + + String version; + + public static Nexx360ExtRequestCaller of(String version) { + return Nexx360ExtRequestCaller.of("Prebid-Server", version); + } +} diff --git a/src/main/java/org/prebid/server/bidder/optidigital/OptidigitalBidder.java b/src/main/java/org/prebid/server/bidder/optidigital/OptidigitalBidder.java new file mode 100644 index 00000000000..8e6e3572fea --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/optidigital/OptidigitalBidder.java @@ -0,0 +1,66 @@ +package org.prebid.server.bidder.optidigital; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class OptidigitalBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OptidigitalBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .toList(); + } + +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java index 09e43473dfa..ffd6c08d184 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -42,6 +42,7 @@ import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.ExtIgi; import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; @@ -504,6 +505,7 @@ public CompositeBidderResponse makeBidderResponse(BidderCall httpCal return CompositeBidderResponse.builder() .bids(extractBids(bidResponse, errors)) .igi(extractIgi(bidResponse)) + .errors(errors) .build(); } catch (DecodeException | PreBidException e) { return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); @@ -523,6 +525,7 @@ private List bidsFromResponse(PubmaticBidResponse bidResponse, List resolveBidderBid(bid, bidResponse.getCur(), bidderErrors)) + .filter(Objects::nonNull) .toList(); } @@ -533,21 +536,22 @@ private BidderBid resolveBidderBid(Bid bid, String currency, List b : null; final PubmaticBidExt pubmaticBidExt = parseBidExt(bid.getExt(), bidderErrors); - final Integer duration = getDuration(pubmaticBidExt); - final BidType bidType = getBidType(pubmaticBidExt); + final BidType bidType = getBidType(bid, bidderErrors); + + if (bidType == null) { + return null; + } final String bidAdm = bid.getAdm(); final String resolvedAdm = bidAdm != null && bidType == BidType.xNative ? resolveNativeAdm(bidAdm, bidderErrors) : bidAdm; - final Bid updatedBid = firstCat != null || duration != null || resolvedAdm != null - ? bid.toBuilder() + final Bid updatedBid = bid.toBuilder() .cat(firstCat) .adm(resolvedAdm != null ? resolvedAdm : bidAdm) - .ext(duration != null ? updateBidExtWithExtPrebid(duration, bid.getExt()) : bid.getExt()) - .build() - : bid; + .ext(updateBidExtWithExtPrebid(pubmaticBidExt, bidType, bid.getExt())) + .build(); return BidderBid.builder() .bid(updatedBid) @@ -567,22 +571,17 @@ private PubmaticBidExt parseBidExt(ObjectNode bidExt, List errors) } } - private static Integer getDuration(PubmaticBidExt bidExt) { - return Optional.ofNullable(bidExt) - .map(PubmaticBidExt::getVideo) - .map(VideoCreativeInfo::getDuration) - .orElse(null); - } - - private static BidType getBidType(PubmaticBidExt bidExt) { - final int bidType = Optional.ofNullable(bidExt) - .map(PubmaticBidExt::getBidType) - .orElse(0); - - return switch (bidType) { - case 1 -> BidType.video; - case 2 -> BidType.xNative; - default -> BidType.banner; + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse("failed to parse bid mtype (%d) for impression id %s" + .formatted(bid.getMtype(), bid.getImpid()))); + yield null; + } }; } @@ -603,9 +602,33 @@ private String resolveNativeAdm(String adm, List bidderErrors) { return null; } - private ObjectNode updateBidExtWithExtPrebid(Integer duration, ObjectNode extBid) { - final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().video(ExtBidPrebidVideo.of(duration, null)).build(); - return extBid.set(PREBID, mapper.mapper().valueToTree(extBidPrebid)); + private ObjectNode updateBidExtWithExtPrebid(PubmaticBidExt pubmaticBidExt, BidType type, ObjectNode extBid) { + final Integer duration = getDuration(pubmaticBidExt); + final boolean inBannerVideo = getInBannerVideo(pubmaticBidExt); + + final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder() + .video(duration != null ? ExtBidPrebidVideo.of(duration, null) : null) + .meta(ExtBidPrebidMeta.builder() + .mediaType(inBannerVideo ? BidType.video.getName() : type.getName()) + .build()) + .build(); + + return extBid != null + ? extBid.set(PREBID, mapper.mapper().valueToTree(extBidPrebid)) + : mapper.mapper().createObjectNode().set(PREBID, mapper.mapper().valueToTree(extBidPrebid)); + } + + private static Integer getDuration(PubmaticBidExt bidExt) { + return Optional.ofNullable(bidExt) + .map(PubmaticBidExt::getVideo) + .map(VideoCreativeInfo::getDuration) + .orElse(null); + } + + private static boolean getInBannerVideo(PubmaticBidExt bidExt) { + return Optional.ofNullable(bidExt) + .map(PubmaticBidExt::getInBannerVideo) + .orElse(false); } private static Integer getDealPriority(PubmaticBidExt bidExt) { diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java index 3aa8db2a2f5..a368a98cc68 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java @@ -1,20 +1,18 @@ package org.prebid.server.bidder.pubmatic.model.response; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @Value(staticConstructor = "of") public class PubmaticBidExt { - @JsonProperty("BidType") - @JsonAlias({"bidtype", "bidType"}) - Integer bidType; - VideoCreativeInfo video; @JsonProperty("prebiddealpriority") Integer prebidDealPriority; String marketplace; + + @JsonProperty("ibv") + Boolean inBannerVideo; } diff --git a/src/main/java/org/prebid/server/bidder/rediads/RediadsBidder.java b/src/main/java/org/prebid/server/bidder/rediads/RediadsBidder.java new file mode 100644 index 00000000000..c0dad6f8760 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rediads/RediadsBidder.java @@ -0,0 +1,169 @@ +package org.prebid.server.bidder.rediads; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.rediads.ExtImpRediads; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class RediadsBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String SUBDOMAIN_MACRO = "{{SUBDOMAIN}}"; + + private final String endpointUrl; + private final String defaultSubdomain; + private final JacksonMapper mapper; + + public RediadsBidder(String endpointUrl, JacksonMapper mapper, String defaultSubdomain) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.defaultSubdomain = Objects.requireNonNull(defaultSubdomain); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + String accountId = null; + String endpoint = null; + + for (Imp imp : request.getImp()) { + try { + final ExtImpRediads extImp = parseImpExt(imp); + modifiedImps.add(modifyImp(imp, extImp)); + accountId = extImp.getAccountId(); + endpoint = extImp.getEndpoint(); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (modifiedImps.isEmpty()) { + return Result.withErrors(errors); + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, accountId); + final String endpointUrl = resolveEndpointUrl(endpoint); + final HttpRequest httpRequest = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private ExtImpRediads parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid imp.ext for impression " + imp.getId()); + } + } + + private Imp modifyImp(Imp imp, ExtImpRediads extImp) { + final ObjectNode modifiedExt = imp.getExt().deepCopy(); + modifiedExt.remove("bidder"); + modifiedExt.remove("prebid"); + return imp.toBuilder() + .tagid(StringUtils.defaultIfBlank(extImp.getSlot(), imp.getTagid())) + .ext(modifiedExt) + .build(); + } + + private BidRequest modifyRequest(BidRequest request, List imps, String accountId) { + final Site site = request.getSite(); + final App app = request.getApp(); + return request.toBuilder() + .site(site != null ? modifySite(site, accountId) : null) + .app(site == null && app != null ? modifyApp(app, accountId) : app) + .imp(imps) + .build(); + } + + private static Site modifySite(Site site, String accountId) { + final Publisher originalPublisher = site.getPublisher(); + final Publisher newPublisher = originalPublisher != null + ? originalPublisher.toBuilder().id(accountId).build() + : Publisher.builder().id(accountId).build(); + return site.toBuilder().publisher(newPublisher).build(); + } + + private static App modifyApp(App app, String accountId) { + final Publisher originalPublisher = app.getPublisher(); + final Publisher newPublisher = originalPublisher != null + ? originalPublisher.toBuilder().id(accountId).build() + : Publisher.builder().id(accountId).build(); + return app.toBuilder().publisher(newPublisher).build(); + } + + private String resolveEndpointUrl(String subdomain) { + return endpointUrl.replace(SUBDOMAIN_MACRO, StringUtils.defaultIfBlank(subdomain, defaultSubdomain)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException( + "could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java b/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java index a3d02f5250d..cff5866d027 100644 --- a/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java +++ b/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java @@ -6,17 +6,14 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.stroeercore.model.StroeerCoreBid; import org.prebid.server.bidder.stroeercore.model.StroeerCoreBidResponse; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -26,7 +23,6 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -41,14 +37,10 @@ public class StroeerCoreBidder implements Bidder { private final String endpointUrl; private final JacksonMapper mapper; - private final CurrencyConversionService currencyConversionService; - public StroeerCoreBidder(String endpointUrl, - JacksonMapper mapper, - CurrencyConversionService currencyConversionService) { + public StroeerCoreBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(endpointUrl); this.mapper = Objects.requireNonNull(mapper); - this.currencyConversionService = Objects.requireNonNull(currencyConversionService); } @Override @@ -57,22 +49,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List errors = new ArrayList<>(); for (Imp imp : bidRequest.getImp()) { - final ExtImpStroeerCore impExt; - final Price price; - try { - validateImp(imp); - - impExt = parseImpExt(imp); - validateImpExt(impExt); - - price = convertBidFloor(bidRequest, imp); + final ExtImpStroeerCore impExt = parseImpExt(imp); + modifiedImps.add(imp.toBuilder().tagid(impExt.getSlotId()).build()); } catch (PreBidException e) { errors.add(BidderError.badInput("%s. Ignore imp id = %s.".formatted(e.getMessage(), imp.getId()))); - continue; } - - modifiedImps.add(modifyImp(imp, impExt, price)); } if (modifiedImps.isEmpty()) { @@ -80,14 +62,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ } final BidRequest outgoingRequest = bidRequest.toBuilder().imp(modifiedImps).build(); - - return createHttpRequests(errors, outgoingRequest); - } - - private static void validateImp(Imp imp) { - if (imp.getBanner() == null && imp.getVideo() == null) { - throw new PreBidException("Expected banner or video impression"); - } + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); } private ExtImpStroeerCore parseImpExt(Imp imp) { @@ -98,65 +73,39 @@ private ExtImpStroeerCore parseImpExt(Imp imp) { } } - private static void validateImpExt(ExtImpStroeerCore impExt) { - if (StringUtils.isBlank(impExt.getSlotId())) { - throw new PreBidException("Custom param slot id (sid) is empty"); - } - } - - private Price convertBidFloor(BidRequest bidRequest, Imp imp) { - final BigDecimal bidFloor = imp.getBidfloor(); - final String bidFloorCurrency = imp.getBidfloorcur(); - - if (!shouldConvertBidFloor(bidFloor, bidFloorCurrency)) { - return Price.of(bidFloorCurrency, bidFloor); - } - - final BigDecimal convertedBidFloor = currencyConversionService.convertCurrency( - bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); - - return Price.of(BIDDER_CURRENCY, convertedBidFloor); - } - - private Result>> createHttpRequests(List errors, BidRequest bidRequest) { - return Result.of(Collections.singletonList(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)), errors); - } - - private static boolean shouldConvertBidFloor(BigDecimal bidFloor, String bidFloorCurrency) { - return BidderUtil.isValidPrice(bidFloor) && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY); - } - - private static Imp modifyImp(Imp imp, ExtImpStroeerCore impExt, Price price) { - return imp.toBuilder() - .bidfloorcur(price.getCurrency()) - .bidfloor(price.getValue()) - .tagid(impExt.getSlotId()) - .build(); - } - @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final String body = httpCall.getResponse().getBody(); + final List errors = new ArrayList<>(); final StroeerCoreBidResponse bidResponse = mapper.decodeValue(body, StroeerCoreBidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + return Result.of(extractBids(bidResponse, errors), errors); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, StroeerCoreBidResponse bidResponse) { + private List extractBids(StroeerCoreBidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getBids())) { return Collections.emptyList(); } return bidResponse.getBids().stream() .filter(Objects::nonNull) - .map(stroeerCoreBid -> toBidderBid(bidRequest, stroeerCoreBid)) + .map(stroeerCoreBid -> toBidderBid(stroeerCoreBid, errors)) + .filter(Objects::nonNull) .toList(); } - private BidderBid toBidderBid(BidRequest bidRequest, StroeerCoreBid stroeercoreBid) { + private BidderBid toBidderBid(StroeerCoreBid stroeercoreBid, List errors) { + final BidType bidType = getBidType(stroeercoreBid.getMtype()); + if (bidType == null) { + errors.add(BidderError.badServerResponse( + "Bid media type error: unable to determine media type for bid with id \"%s\"" + .formatted(stroeercoreBid.getBidId()))); + return null; + } + final ObjectNode bidExt = stroeercoreBid.getDsa() != null ? mapper.mapper().createObjectNode().set("dsa", stroeercoreBid.getDsa()) : null; @@ -164,29 +113,25 @@ private BidderBid toBidderBid(BidRequest bidRequest, StroeerCoreBid stroeercoreB return BidderBid.of( Bid.builder() .id(stroeercoreBid.getId()) - .impid(stroeercoreBid.getImpId()) + .impid(stroeercoreBid.getBidId()) .w(stroeercoreBid.getWidth()) .h(stroeercoreBid.getHeight()) .price(stroeercoreBid.getCpm()) .adm(stroeercoreBid.getAdMarkup()) .crid(stroeercoreBid.getCreativeId()) + .adomain(stroeercoreBid.getAdomain()) + .mtype(bidType.ordinal() + 1) .ext(bidExt) .build(), - getBidType(stroeercoreBid.getImpId(), bidRequest.getImp()), + bidType, BIDDER_CURRENCY); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } - } - } - - return BidType.banner; + private static BidType getBidType(String mtype) { + return switch (mtype) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + default -> null; + }; } } diff --git a/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java b/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java index 84a875b2b80..2c665a27ce5 100644 --- a/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java +++ b/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java @@ -6,6 +6,7 @@ import lombok.Value; import java.math.BigDecimal; +import java.util.List; @Value @Builder @@ -14,7 +15,7 @@ public class StroeerCoreBid { String id; @JsonProperty("bidId") - String impId; + String bidId; BigDecimal cpm; @@ -29,4 +30,8 @@ public class StroeerCoreBid { String creativeId; ObjectNode dsa; + + String mtype; + + List adomain; } diff --git a/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java b/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java new file mode 100644 index 00000000000..c665b06650c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java @@ -0,0 +1,107 @@ +package org.prebid.server.bidder.zentotem; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ZentotemBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ZentotemBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + httpRequests.add(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null + ? BidderBid.of(bid, bidType, currency) + : null; + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse( + "could not define media type for impression: " + bid.getImpid())); + yield null; + } + }; + } +} diff --git a/src/main/java/org/prebid/server/cookie/CookieSyncService.java b/src/main/java/org/prebid/server/cookie/CookieSyncService.java index fb15b5478d9..4ebd38c45ed 100644 --- a/src/main/java/org/prebid/server/cookie/CookieSyncService.java +++ b/src/main/java/org/prebid/server/cookie/CookieSyncService.java @@ -111,6 +111,8 @@ public Future processContext(CookieSyncContext cookieSyncCont .map(this::filterDisabledBidders) .map(this::filterBiddersWithoutUsersync) .map(this::filterBiddersWithDisabledUsersync) + .map(this::filterBiddersByGdpr) + .map(this::filterBiddersByGppSid) .map(this::applyRequestFilterSettings) .compose(this::applyPrivacyFilteringRules) .map(this::filterInSyncBidders); @@ -202,6 +204,26 @@ private CookieSyncContext filterBiddersWithDisabledUsersync(CookieSyncContext co RejectionReason.DISABLED_USERSYNC); } + private CookieSyncContext filterBiddersByGdpr(CookieSyncContext cookieSyncContext) { + return filterBidders( + cookieSyncContext, + bidder -> cookieSyncContext.getPrivacyContext().getTcfContext().isInGdprScope() + && bidderCatalog.usersyncerByName(bidder).map(Usersyncer::isSkipWhenInGdprScope).orElse(false), + RejectionReason.REJECTED_BY_REGULATION_SCOPE); + } + + private CookieSyncContext filterBiddersByGppSid(CookieSyncContext cookieSyncContext) { + return filterBidders( + cookieSyncContext, + bidder -> bidderCatalog.usersyncerByName(bidder) + .map(Usersyncer::getGppSidToSkip) + .map(gppSid -> !Collections.disjoint( + gppSid, + cookieSyncContext.getCookieSyncRequest().getGppSid())) + .orElse(false), + RejectionReason.REJECTED_BY_REGULATION_SCOPE); + } + /** * should be called after applying request filter, as it will populate usersync data */ @@ -469,6 +491,8 @@ private BidderUsersyncStatus rejectionStatus(String bidder, RejectionReason reas case DISABLED_USERSYNC -> builder.conditionalError(requested || coopSync, "Sync disabled by config"); case REJECTED_BY_FILTER -> builder.conditionalError(requested || coopSync, "Rejected by request filter"); case ALREADY_IN_SYNC -> builder.conditionalError(requested, "Already in sync"); + case REJECTED_BY_REGULATION_SCOPE -> builder.conditionalError( + requested || coopSync, "Rejected by regulation scope"); }; return builder.build(); diff --git a/src/main/java/org/prebid/server/cookie/model/RejectionReason.java b/src/main/java/org/prebid/server/cookie/model/RejectionReason.java index 8c503642c29..5130a3abb93 100644 --- a/src/main/java/org/prebid/server/cookie/model/RejectionReason.java +++ b/src/main/java/org/prebid/server/cookie/model/RejectionReason.java @@ -10,5 +10,6 @@ public enum RejectionReason { UNCONFIGURED_USERSYNC, DISABLED_USERSYNC, REJECTED_BY_FILTER, - ALREADY_IN_SYNC + ALREADY_IN_SYNC, + REJECTED_BY_REGULATION_SCOPE } diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 84bb68f1c67..ab32c446226 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -31,6 +31,7 @@ public enum MetricName { request_time, prices, imps_requested, + imps_dropped, imps_banner, imps_video, imps_native, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 00295decad3..9010a7018c5 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -182,6 +182,10 @@ public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean l incCounter(MetricName.imps_requested, numImps); } + public void updateImpsDroppedMetric(int numImps) { + incCounter(MetricName.imps_dropped, numImps); + } + public void updateImpTypesMetrics(List imps) { final Map mediaTypeToCount = imps.stream() diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/akcelo/ExtImpAkcelo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/akcelo/ExtImpAkcelo.java new file mode 100644 index 00000000000..4e57d8d6a12 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/akcelo/ExtImpAkcelo.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.akcelo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAkcelo { + + @JsonProperty("adUnitId") + Integer adUnitId; + + @JsonProperty("siteId") + String siteId; + + Integer test; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediasquare/ExtImpMediasquare.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediasquare/ExtImpMediasquare.java new file mode 100644 index 00000000000..1c71f176881 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediasquare/ExtImpMediasquare.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.mediasquare; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMediasquare { + + String owner; + + String code; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java new file mode 100644 index 00000000000..5a44dfd868f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.nexx360; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpNexx360 { + + @JsonProperty("tagId") + String tagId; + + String placement; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/rediads/ExtImpRediads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rediads/ExtImpRediads.java new file mode 100644 index 00000000000..e876780fac8 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rediads/ExtImpRediads.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.rediads; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpRediads { + + String accountId; + + String slot; + + String endpoint; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index e41f005df54..397047bf240 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -60,4 +60,7 @@ public class AccountAuctionConfig { AccountCacheConfig cache; AccountBidRankingConfig ranking; + + @JsonAlias("impression-limit") + Integer impressionLimit; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AkceloConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AkceloConfiguration.java new file mode 100644 index 00000000000..3b6472eac49 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AkceloConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.akcelo.AkceloBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/akcelo.yaml", + factory = YamlPropertySourceFactory.class) +public class AkceloConfiguration { + + private static final String BIDDER_NAME = "akcelo"; + + @Bean("akceloConfigurationProperties") + @ConfigurationProperties("adapters.akcelo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps akceloBidderDeps(BidderConfigurationProperties akceloConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(akceloConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AkceloBidder(config.getEndpoint(), mapper)) + .assemble(); + } + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MediasquareConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MediasquareConfiguration.java new file mode 100644 index 00000000000..bbdd5b6ed2b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MediasquareConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.mediasquare.MediasquareBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/mediasquare.yaml", factory = YamlPropertySourceFactory.class) +public class MediasquareConfiguration { + + private static final String BIDDER_NAME = "mediasquare"; + + @Bean("mediasquareConfigurationProperties") + @ConfigurationProperties("adapters.mediasquare") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps mediasquareBidderDeps(BidderConfigurationProperties mediasquareConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(mediasquareConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MediasquareBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java b/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java new file mode 100644 index 00000000000..2eccf05c68c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java @@ -0,0 +1,46 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.nexx360.Nexx360Bidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.version.PrebidVersionProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/nexx360.yaml", factory = YamlPropertySourceFactory.class) +public class Nexx360Configuration { + + private static final String BIDDER_NAME = "nexx360"; + + @Bean("nexx360ConfigurationProperties") + @ConfigurationProperties("adapters.nexx360") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps nexx360BidderDeps(BidderConfigurationProperties nexx360ConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(nexx360ConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new Nexx360Bidder( + config.getEndpoint(), + mapper, + prebidVersionProvider)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OptidigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OptidigitalConfiguration.java new file mode 100644 index 00000000000..dbd9309821b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OptidigitalConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.optidigital.OptidigitalBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/optidigital.yaml", factory = YamlPropertySourceFactory.class) +public class OptidigitalConfiguration { + + private static final String BIDDER_NAME = "optidigital"; + + @Bean("optidigitalConfigurationProperties") + @ConfigurationProperties("adapters.optidigital") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps optidigitalBidderDeps(BidderConfigurationProperties optidigitalConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(optidigitalConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OptidigitalBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RediadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RediadsConfiguration.java new file mode 100644 index 00000000000..8339e77c199 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/RediadsConfiguration.java @@ -0,0 +1,56 @@ +package org.prebid.server.spring.config.bidder; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.rediads.RediadsBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/rediads.yaml", factory = YamlPropertySourceFactory.class) +public class RediadsConfiguration { + + private static final String BIDDER_NAME = "rediads"; + + @Bean("rediadsConfigurationProperties") + @ConfigurationProperties("adapters.rediads") + RediadsConfigurationProperties configurationProperties() { + return new RediadsConfigurationProperties(); + } + + @Bean + BidderDeps rediadsBidderDeps(RediadsConfigurationProperties rediadsConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(rediadsConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new RediadsBidder( + config.getEndpoint(), + mapper, + config.getDefaultSubdomain())) + .assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class RediadsConfigurationProperties extends BidderConfigurationProperties { + + @NotBlank + private String defaultSubdomain; + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java index f33c72e5027..49369a2ddac 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/StroeerCoreConfiguration.java @@ -2,7 +2,6 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.stroeercore.StroeerCoreBidder; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -31,13 +30,12 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps stroeercoreBidderDeps(BidderConfigurationProperties stroeercoreConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, - CurrencyConversionService currencyConversionService, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(stroeercoreConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new StroeerCoreBidder(config.getEndpoint(), mapper, currencyConversionService)) + .bidderCreator(config -> new StroeerCoreBidder(config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZentotemConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZentotemConfiguration.java new file mode 100644 index 00000000000..1578640e0ad --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ZentotemConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.zentotem.ZentotemBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/zentotem.yaml", factory = YamlPropertySourceFactory.class) +public class ZentotemConfiguration { + + private static final String BIDDER_NAME = "zentotem"; + + @Bean("zentotemConfigurationProperties") + @ConfigurationProperties("adapters.zentotem") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps zentotemBidderDeps(BidderConfigurationProperties zentotemConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(zentotemConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ZentotemBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncBidderRegulationScopeProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncBidderRegulationScopeProperties.java new file mode 100644 index 00000000000..49ab5fd74b4 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncBidderRegulationScopeProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.spring.config.bidder.model.usersync; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class UsersyncBidderRegulationScopeProperties { + + boolean gdpr; + + List gppSid; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java index e67debfad6c..a93fac2f41c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncConfigurationProperties.java @@ -19,4 +19,6 @@ public class UsersyncConfigurationProperties { UsersyncMethodConfigurationProperties redirect; UsersyncMethodConfigurationProperties iframe; + + UsersyncBidderRegulationScopeProperties skipwhen; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java index 462bd3d3cdc..b5606c719b9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java @@ -6,6 +6,7 @@ import org.prebid.server.bidder.UsersyncUtil; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; +import org.prebid.server.spring.config.bidder.model.usersync.UsersyncBidderRegulationScopeProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncConfigurationProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncMethodConfigurationProperties; import org.prebid.server.util.HttpUtil; @@ -30,13 +31,16 @@ private static Usersyncer createAndValidate(UsersyncConfigurationProperties user String externalUrl) { final String cookieFamilyName = usersync.getCookieFamilyName(); + final UsersyncBidderRegulationScopeProperties skipwhenConfig = usersync.getSkipwhen(); return Usersyncer.of( usersync.getEnabled(), cookieFamilyName, cookieFamilySource, toMethod(UsersyncMethodType.IFRAME, usersync.getIframe(), cookieFamilyName, externalUrl), - toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName, externalUrl)); + toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName, externalUrl), + skipwhenConfig != null && skipwhenConfig.isGdpr(), + skipwhenConfig == null ? null : skipwhenConfig.getGppSid()); } private static UsersyncMethod toMethod(UsersyncMethodType type, diff --git a/src/main/resources/bidder-config/akcelo.yaml b/src/main/resources/bidder-config/akcelo.yaml new file mode 100644 index 00000000000..dfec684d42d --- /dev/null +++ b/src/main/resources/bidder-config/akcelo.yaml @@ -0,0 +1,12 @@ +adapters: + akcelo: + endpoint: https://s2s.sportslocalmedia.com/openrtb2/auction + meta-info: + maintainer-email: tech@akcelo.io + app-media-types: + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/mediasquare.yaml b/src/main/resources/bidder-config/mediasquare.yaml new file mode 100644 index 00000000000..b8be22071d8 --- /dev/null +++ b/src/main/resources/bidder-config/mediasquare.yaml @@ -0,0 +1,13 @@ +adapters: + mediasquare: + endpoint: "https://pbs-front.mediasquare.fr/msq_prebid" + endpoint-compression: gzip + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: tech@mediasquare.fr + app-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 791 diff --git a/src/main/resources/bidder-config/nexx360.yaml b/src/main/resources/bidder-config/nexx360.yaml new file mode 100644 index 00000000000..443ca7f5ab4 --- /dev/null +++ b/src/main/resources/bidder-config/nexx360.yaml @@ -0,0 +1,22 @@ +adapters: + nexx360: + endpoint: http://fast.nexx360.io/prebid-server + endpoint-compression: gzip + aliases: + 1accord: ~ + easybid: ~ + prismassp: ~ + meta-info: + maintainer-email: tech@nexx360.io + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml index a504fcdf8fd..2a5002667e0 100644 --- a/src/main/resources/bidder-config/openx.yaml +++ b/src/main/resources/bidder-config/openx.yaml @@ -18,9 +18,9 @@ adapters: usersync: cookie-family-name: openx iframe: - url: https://u.openx.net/w/1.0/cm?id=891039ac-a916-42bb-a651-4be9e3b201da&ph=a3aece0c-9e80-4316-8deb-faf804779bd1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&r={{redirect_url}} + url: https://u.openx.net/w/1.0/cm?id=891039ac-a916-42bb-a651-4be9e3b201da&ph=a3aece0c-9e80-4316-8deb-faf804779bd1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false redirect: - url: https://rtb.openx.net/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&r={{redirect_url}} + url: https://rtb.openx.net/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false uid-macro: '${UID}' diff --git a/src/main/resources/bidder-config/optidigital.yaml b/src/main/resources/bidder-config/optidigital.yaml new file mode 100644 index 00000000000..9e48244221b --- /dev/null +++ b/src/main/resources/bidder-config/optidigital.yaml @@ -0,0 +1,22 @@ +adapters: + optidigital: + enabled: false + endpoint: https://pbs.optidigital.com/bidder/openrtb2 + endpoint-compression: gzip + ortb-version: "2.6" + meta-info: + maintainer-email: prebid@optidigital.com + app-media-types: + - banner + site-media-types: + - banner + dooh-media-types: + - banner + supported-vendors: + vendor-id: 915 + usersync: + cookie-family-name: optidigital + iframe: + url: https://scripts.opti-digital.com/js/presyncs2s.html?endpoint=optidigital&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/rediads.yaml b/src/main/resources/bidder-config/rediads.yaml new file mode 100644 index 00000000000..c885129acba --- /dev/null +++ b/src/main/resources/bidder-config/rediads.yaml @@ -0,0 +1,20 @@ +adapters: + rediads: + endpoint: https://{{SUBDOMAIN}}.rediads.com/openrtb2/auction + default-subdomain: bidding + ortb-version: "2.6" + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: support@rediads.com + app-media-types: + - banner + - video + - audio + - native + site-media-types: + - banner + - video + - audio + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/richaudience.yaml b/src/main/resources/bidder-config/richaudience.yaml index b691d330734..4ee2e2ef453 100644 --- a/src/main/resources/bidder-config/richaudience.yaml +++ b/src/main/resources/bidder-config/richaudience.yaml @@ -1,6 +1,6 @@ adapters: richaudience: - endpoint: http://ortb.richaudience.com/ortb/?bidder=pbs + endpoint: https://ortb.richaudience.com/ortb/?bidder=pbs meta-info: maintainer-email: partnerintegrations@richaudience.com app-media-types: diff --git a/src/main/resources/bidder-config/zentotem.yaml b/src/main/resources/bidder-config/zentotem.yaml new file mode 100644 index 00000000000..0d6b0fe8121 --- /dev/null +++ b/src/main/resources/bidder-config/zentotem.yaml @@ -0,0 +1,17 @@ +adapters: + zentotem: + endpoint: https://rtb.zentotem.net/bid?sspuid=cqlnvfk00bhs0b6rci6g + endpoint-compression: gzip + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: support@zentotem.net + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/akcelo.json b/src/main/resources/static/bidder-params/akcelo.json new file mode 100644 index 00000000000..2bdf75ce1d2 --- /dev/null +++ b/src/main/resources/static/bidder-params/akcelo.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Akcelo Adapter Params", + "description": "A schema which validates params accepted by the Akcelo adapter", + "type": "object", + "properties": { + "adUnitID": { + "type": "number", + "description": "The identifier of the ad unit. Will be provided by your account manager." + }, + "siteId": { + "type": "number", + "description": "The identifier of the site. Will be provided by your account manager." + }, + "test": { + "type": "number", + "description": "Whether to display test creatives or not. Default is 0." + } + }, + "required": [ + "adUnitId", + "siteId" + ] +} diff --git a/src/main/resources/static/bidder-params/mediasquare.json b/src/main/resources/static/bidder-params/mediasquare.json new file mode 100644 index 00000000000..ee8d3c67d0d --- /dev/null +++ b/src/main/resources/static/bidder-params/mediasquare.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mediasquare Adapter Params", + "description": "A schema which validates params accepted by the Mediasquare adapter", + "type": "object", + "properties": { + "owner": { + "type": "string", + "minLength": 1, + "description": "The owner provided for mediasquare." + }, + "code": { + "type": "string", + "minLength": 1, + "description": "The code provided for mediasquare." + } + }, + "required": [ + "owner", + "code" + ] +} diff --git a/src/main/resources/static/bidder-params/nexx360.json b/src/main/resources/static/bidder-params/nexx360.json new file mode 100644 index 00000000000..32e06db5297 --- /dev/null +++ b/src/main/resources/static/bidder-params/nexx360.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Nexx360 Adapter Params", + "description": "A schema which validates params accepted by the Nexx360 adapter", + "type": "object", + "properties": { + "tagId": { + "type": "string", + "minLength": 1, + "description": "TagId" + }, + "placement": { + "type": "string", + "minLength": 1, + "description": "Placement" + } + }, + "anyOf": [ + { + "required": [ + "tagId" + ] + }, + { + "required": [ + "placement" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/optidigital.json b/src/main/resources/static/bidder-params/optidigital.json new file mode 100644 index 00000000000..4cde58a8100 --- /dev/null +++ b/src/main/resources/static/bidder-params/optidigital.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Optidigital Adapter Params", + "description": "A schema which validates params accepted by the Optidigital adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "minLength": 2, + "description": "Publisher ID" + }, + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "divId": { + "type": "string", + "description": "Div ID" + }, + "pageTemplate": { + "type": "string", + "description": "Page Template" + } + }, + "required": [ + "publisherId", + "placementId" + ] +} diff --git a/src/main/resources/static/bidder-params/rediads.json b/src/main/resources/static/bidder-params/rediads.json new file mode 100644 index 00000000000..13a8f9b4cbe --- /dev/null +++ b/src/main/resources/static/bidder-params/rediads.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "RediAds Adapter Params", + "description": "A schema which validates params accepted by the Rediads adapter", + "type": "object", + "properties": { + "account_id": { + "type": "string" + }, + "slot": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + }, + "required": [ + "account_id" + ] +} diff --git a/src/main/resources/static/bidder-params/stroeerCore.json b/src/main/resources/static/bidder-params/stroeerCore.json index e693962fa97..a5a93df64b8 100644 --- a/src/main/resources/static/bidder-params/stroeerCore.json +++ b/src/main/resources/static/bidder-params/stroeerCore.json @@ -9,5 +9,7 @@ "description": "Slot Id" } }, - "required": ["sid"] + "required": [ + "sid" + ] } diff --git a/src/main/resources/static/bidder-params/zentotem.json b/src/main/resources/static/bidder-params/zentotem.json new file mode 100644 index 00000000000..de59fc47b70 --- /dev/null +++ b/src/main/resources/static/bidder-params/zentotem.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Zentotem Adapter Params", + "description": "A schema which validates params accepted by the Zentotem adapter", + "type": "object", + "properties": {} +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index bf49ce7c874..21a60bef192 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -35,6 +35,7 @@ class AccountAuctionConfig { @JsonProperty("bidadjustments") BidAdjustment bidAdjustments BidRounding bidRounding + Integer impressionLimit @JsonProperty("price_granularity") PriceGranularityType priceGranularitySnakeCase @@ -54,4 +55,7 @@ class AccountAuctionConfig { AccountPriceFloorsConfig priceFloorsSnakeCase @JsonProperty("bid_rounding") BidRounding bidRoundingSnakeCase + @JsonProperty("impression_limit") + Integer impressionLimitSnakeCase + } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy index a9bc17dfe1d..1506e2e0a4d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -9,6 +9,7 @@ import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.DeviceExt +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.PrebidStoredRequest import org.prebid.server.functional.model.request.auction.Renderer import org.prebid.server.functional.model.request.auction.RendererData @@ -47,15 +48,18 @@ class AuctionSpec extends BaseSpec { private static final Integer DEFAULT_TIMEOUT = getRandomTimeout() private static final Integer MIN_BID_ID_LENGTH = 17 private static final Integer DEFAULT_UUID_LENGTH = 36 - private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, - "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] private static final Map GENERIC_CONFIG = [ "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] - @Shared PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG) + private static final String IMPS_REQUESTED_METRIC = 'imps_requested' + private static final String IMPS_DROPPED_METRIC = 'imps_dropped' + private static final Integer IMP_LIMIT = 1 + private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, + "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] + def "PBS should return version in response header for auction request for #description"() { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequestRaw(bidRequest) @@ -721,4 +725,163 @@ class AuctionSpec extends BaseSpec { cleanup: "Stop and remove pbs container" pbsServiceFactory.removeContainer(pbsConfig) } + + def "PBS should drop extra impressions with warnings when number of impressions exceeds impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "PBS should emit an warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == + ["Only first $IMP_LIMIT impressions were kept due to the limit, " + + "all the subsequent impressions have been dropped for the auction" as String] + + and: "PBS shouldn't emit an error" + assert !response.ext?.errors + + and: "Metrics for imps should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_DROPPED_METRIC] == bidRequest.imp.size() - IMP_LIMIT + assert metrics[IMPS_REQUESTED_METRIC] == IMP_LIMIT + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == IMP_LIMIT + + and: "Bidder request should contain imps according to limit" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == IMP_LIMIT + + where: + accountAuctionConfig << [ + new AccountAuctionConfig(impressionLimit: IMP_LIMIT), + new AccountAuctionConfig(impressionLimitSnakeCase: IMP_LIMIT) + ] + } + + def "PBS shouldn't drop extra impressions when number of impressions equal to impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: bidRequest.imp.size())) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + } + + def "PBS shouldn't drop extra impressions when number of impressions less than or equal to impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def impressionLimit = bidRequest.imp.size() + 1 + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + } + + def "PBS shouldn't drop extra impressions when impression-limit set to #impressionLimit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + + where: + impressionLimit << [null, PBSUtils.randomNegativeNumber, 0] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy index a462e91a1ba..a5a6a13ca09 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppCookieSyncSpec.groovy @@ -1,6 +1,12 @@ package org.prebid.server.functional.tests.privacy import io.netty.handler.codec.http.HttpResponseStatus +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountGdprConfig +import org.prebid.server.functional.model.config.AccountPrivacyConfig +import org.prebid.server.functional.model.config.PurposeConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.GppSectionId import org.prebid.server.functional.model.request.cookiesync.CookieSyncRequest import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo import org.prebid.server.functional.service.PrebidServerException @@ -12,8 +18,12 @@ import org.prebid.server.functional.util.privacy.CcpaConsent import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent import org.prebid.server.functional.util.privacy.gpp.UsV1Consent +import spock.lang.IgnoreRest +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.Purpose.P1 +import static org.prebid.server.functional.model.config.PurposeEnforcement.NO import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.IFRAME @@ -28,15 +38,26 @@ class GppCookieSyncSpec extends BaseSpec { private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT private static final boolean CORS_SUPPORT = false private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" + private static final GppSectionId FIRST_GPP_SECTION = PBSUtils.getRandomEnum(GppSectionId.class) + private static final GppSectionId SECOND_GPP_SECTION = PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION]) + private static final Map GENERIC_CONFIG = [ "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + private static final Map GENERIC_WITH_SKIP_CONFIG = [ + "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), + "adapters.${GENERIC.value}.usersync.skipwhen.gdpr" : 'true', + "adapters.${GENERIC.value}.usersync.skipwhen.gpp_sid" : "${FIRST_GPP_SECTION.value}, ${SECOND_GPP_SECTION.value}".toString(), + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] private static PrebidServerService prebidServerService = pbsServiceFactory.getService(GENERIC_CONFIG) + private static PrebidServerService prebidServerServiceWithSkipConfig = pbsServiceFactory.getService(GENERIC_WITH_SKIP_CONFIG + GENERIC_ALIAS_CONFIG) def cleanupSpec() { pbsServiceFactory.removeContainer(GENERIC_CONFIG) + pbsServiceFactory.removeContainer(GENERIC_WITH_SKIP_CONFIG + GENERIC_ALIAS_CONFIG) } def "PBS cookie sync request should set GDPR to 1 when gpp_sid contains 2"() { @@ -171,8 +192,8 @@ class GppCookieSyncSpec extends BaseSpec { it.gpp = new TcfEuV2Consent.Builder().build() it.gdpr = null it.gdprConsent = new TcfConsent.Builder().setPurposesLITransparency(DEVICE_ACCESS) - .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) - .build() + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() } when: "PBS processes cookie sync request" @@ -261,4 +282,238 @@ class GppCookieSyncSpec extends BaseSpec { where: userSyncFormat << [REDIRECT, IFRAME] } + + def "PBS should emit proper error message when request contain gdpr config and global skip gdpr config for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + } + + def "PBS should emit proper error message when alias request contain gdpr config and global skip gdpr config for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.bidders = [ALIAS] + it.gppSid = TCF_EU_V2.intValue + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + } + + def "PBS should emit proper error message when request contain gpp config and specific global skip gpp config for adapter"() { + given: "Default CookieSyncRequest with gpp and gppSid" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.gpp = new UsV1Consent.Builder().build() + it.gppSid = gppSid + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + + where: + gppSid << ["${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${SECOND_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}, ${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}" + ] + } + + def "PBS should emit proper error message when alias request contain gpp config and specific global skip gpp config for adapter"() { + given: "Default CookieSyncRequest with gpp and gppSid" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.bidders = [ALIAS] + it.gpp = new UsV1Consent.Builder().build() + it.gppSid = gppSid + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + + where: + gppSid << ["${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${SECOND_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}, ${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}" + ] + } + + def "PBS shouldn't emit error message when request doesn't contain gdpr config and global skip gdpr config for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def gppSid = TCF_EU_V2 + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = gppSid.intValue + it.gdpr = 0 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url should contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp") == "" + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp_sid") == gppSid.value + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + } + + def "PBS shouldn't emit error message when request does contain gdpr config and global skip gdpr config disabled for adapter"() { + given: "Pbs config with usersync.#userSyncFormat.url" + def pbsConfig = [ + "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.skipwhen.gdpr" : 'false', + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.value + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + it.account = PBSUtils.randomNumber + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P1): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def privacyConfig = new AccountPrivacyConfig(gdpr: accountGdprConfig) + def account = new Account(uuid: cookieSyncRequest.account, config: new AccountConfig(privacy: privacyConfig)) + accountDao.save(account) + + when: "PBS processes cookie sync request" + def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain proper userSync url" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert bidderStatus.userSync?.url == USER_SYNC_URL + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't emit error message when request does contain gdpr config and global skip gdpr config default for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.value + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + it.account = PBSUtils.randomNumber + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P1): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def privacyConfig = new AccountPrivacyConfig(gdpr: accountGdprConfig) + def account = new Account(uuid: cookieSyncRequest.account, config: new AccountConfig(privacy: privacyConfig)) + accountDao.save(account) + + when: "PBS processes cookie sync request" + def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain proper userSync url" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert bidderStatus.userSync?.url == USER_SYNC_URL + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + } + + def "PBS shouldn't emit error message when request doesn't contain matched gpp config and specific global skip gpp config for adapter"() { + given: "Default CookieSyncRequest with gpp and gppSid" + def gpp = new UsV1Consent.Builder().build() + def gppSid = "${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.gpp = gpp + it.gppSid = gppSid + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url should contain gpp and gppSid" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp") == gpp.toString() + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp_sid") == gppSid + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + } + + def "PBS should also include validation warning when request matches skip config and has validation issue at same time"() { + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = PBSUtils.getRandomNumberWithExclusion(TCF_EU_V2.intValue) + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain a warning" + assert response.warnings == ["GPP scope does not match TCF2 scope"] + + then: "Privacy for bidder should be enforced" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert bidderStatus.error == "Rejected by regulation scope" + } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index a913772645e..7f3642cb8b0 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -300,7 +300,7 @@ public void setUp() { given(bidderCatalog.isValidName(anyString())).willReturn(true); given(bidderCatalog.isActive(anyString())).willReturn(true); given(bidderCatalog.usersyncerByName(anyString())) - .willReturn(Optional.of(Usersyncer.of("cookieFamily", null, null))); + .willReturn(Optional.of(Usersyncer.of("cookieFamily", null, null, false, null))); given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create( true, null, diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index fe4c72b89ca..b521d8623d6 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -1757,6 +1757,8 @@ private void givenBidRequest( given(ortb2ImplicitParametersResolver.resolve(any(), any(), any(), anyBoolean())).willAnswer( answerWithFirstArgument()); + given(ortb2RequestFactory.limitImpressions(any(), any(), any())) + .willAnswer(invocation -> Future.succeededFuture((BidRequest) invocation.getArgument(1))); given(ortb2RequestFactory.validateRequest(any(), any(), any(), any(), any())) .willAnswer(invocation -> Future.succeededFuture((BidRequest) invocation.getArgument(1))); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 19e414e80c9..7f07e0107e7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -167,6 +167,8 @@ public void setUp() { given(ortb2RequestFactory.executeRawAuctionRequestHooks(any())) .willAnswer(invocation -> Future.succeededFuture( ((AuctionContext) invocation.getArgument(0)).getBidRequest())); + given(ortb2RequestFactory.limitImpressions(any(), any(), any())) + .willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(1))); given(ortb2RequestFactory.validateRequest(any(), any(), any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture((BidRequest) invocationOnMock.getArgument(1))); given(ortb2RequestFactory.removeEmptyEids(any(), any())) diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index d5c2cb5bea3..778916127c7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -7,6 +7,7 @@ import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; @@ -83,6 +84,7 @@ import java.util.List; import java.util.function.UnaryOperator; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; @@ -90,12 +92,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.prebid.server.assertion.FutureAssertion.assertThat; @@ -1708,6 +1712,70 @@ public void removeEmptyEidsShouldRemoveEmptyUidsOnly() { "removed EID source3 due to empty ID"); } + @Test + public void validateShouldDropImpressionsOverAccountLimitAndReturnWarning() { + // given + final Imp imp1 = Imp.builder().id("1").build(); + final Imp imp2 = Imp.builder().id("2").build(); + final BidRequest bidRequest = givenBidRequest(request -> request.imp(asList(imp1, imp2))); + final List warning = new ArrayList<>(); + + final Account givenAccount = Account.builder() + .id(ACCOUNT_ID) + .auction(AccountAuctionConfig.builder().impressionLimit(1).build()) + .build(); + + // when + final BidRequest result = target.limitImpressions(givenAccount, bidRequest, warning).result(); + + // then + assertThat(warning).hasSize(1) + .containsOnly("Only first 1 impressions were kept due to the limit, " + + "all the subsequent impressions have been dropped for the auction"); + + verify(metrics).updateImpsDroppedMetric(1); + assertThat(result.getImp()).containsOnly(imp1); + } + + @Test + public void validateShouldNotDropImpressionsReturnWarningWhenAccountLimitIsSetToZero() { + // given + final Imp imp1 = Imp.builder().id("1").build(); + final Imp imp2 = Imp.builder().id("2").build(); + final BidRequest bidRequest = givenBidRequest(request -> request.imp(asList(imp1, imp2))); + final List warning = new ArrayList<>(); + + final Account givenAccount = Account.builder() + .id(ACCOUNT_ID) + .auction(AccountAuctionConfig.builder().impressionLimit(0).build()) + .build(); + + // when + final BidRequest result = target.limitImpressions(givenAccount, bidRequest, warning).result(); + + // then + assertThat(warning).isEmpty(); + assertThat(result).isEqualTo(bidRequest); + verify(metrics, never()).updateImpsDroppedMetric(anyInt()); + } + + @Test + public void validateShouldNotDropImpressionsReturnWarningWhenAccountLimitIsNotSet() { + // given + final Imp imp1 = Imp.builder().id("1").build(); + final Imp imp2 = Imp.builder().id("2").build(); + final BidRequest bidRequest = givenBidRequest(request -> request.imp(asList(imp1, imp2))); + final List warning = new ArrayList<>(); + + // when + final BidRequest result = target.limitImpressions(Account.empty(ACCOUNT_ID), bidRequest, warning).result(); + + // then + assertThat(warning).isEmpty(); + assertThat(result).isEqualTo(bidRequest); + verify(metrics, never()).updateImpsDroppedMetric(anyInt()); + } + private void givenTarget(int timeoutAdjustmentFactor) { target = new Ortb2RequestFactory( timeoutAdjustmentFactor, diff --git a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java index ca01531b02e..ad154e06b49 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java @@ -408,6 +408,8 @@ private void givenBidRequest(BidRequest bidRequest, List podErrors) { .build()); given(ortb2RequestFactory.fetchAccountWithoutStoredRequestLookup(any())).willReturn(Future.succeededFuture()); + given(ortb2RequestFactory.limitImpressions(any(), any(), any())) + .willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(1))); given(ortb2RequestFactory.validateRequest(any(), any(), any(), any(), any())) .willAnswer(invocation -> Future.succeededFuture((BidRequest) invocation.getArgument(1))); diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java index 347279cd3b5..96861372cd3 100644 --- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java +++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java @@ -218,7 +218,7 @@ public void metaInfoByNameShouldReturnNullForUnknownBidder() { @Test public void usersyncerByNameShouldReturnUsersyncerForKnownBidderIgnoringCase() { // given - final Usersyncer usersyncer = Usersyncer.of("name", null, null); + final Usersyncer usersyncer = Usersyncer.of("name", null, null, false, null); final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("BIDder") .deprecatedNames(emptyList()) @@ -233,7 +233,7 @@ public void usersyncerByNameShouldReturnUsersyncerForKnownBidderIgnoringCase() { @Test public void cookieFamilyNameShouldReturnCookieFamilyForKnownBidderIgnoringCase() { // given - final Usersyncer usersyncer = Usersyncer.of("name", null, null); + final Usersyncer usersyncer = Usersyncer.of("name", null, null, false, null); final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("BIDder") .deprecatedNames(emptyList()) @@ -310,7 +310,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { .name("bidder-with-usersync") .deprecatedNames(emptyList()) .bidderInfo(infoOfBidderWithUsersyncConfig) - .usersyncer(Usersyncer.of("bidder-with-usersync-family", null, null)) + .usersyncer(Usersyncer.of("bidder-with-usersync-family", null, null, false, null)) .build())), BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("bidder-without-usersync") @@ -320,7 +320,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { BidderDeps.of(singletonList(BidderInstanceDeps.builder() .name("disabled-bidder-with-usersync") .bidderInfo(infoOfDisabledBidderWithUsersyncConfig) - .usersyncer(Usersyncer.of("isabled-bidder-with-usersync-family", null, null)) + .usersyncer(Usersyncer.of("isabled-bidder-with-usersync-family", null, null, false, null)) .deprecatedNames(emptyList()) .build()))); diff --git a/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java b/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java index c191159e71d..b129a075dce 100644 --- a/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java +++ b/src/test/java/org/prebid/server/bidder/UsersyncMethodChooserTest.java @@ -58,7 +58,7 @@ public void shouldReturnRedirectMethodWhenIframeMethodFilterExcludeAndNullBidder null, CookieSyncRequest.FilterType.exclude), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -93,7 +93,7 @@ public void shouldReturnSecondaryMethodWhenInMethodFilterExcludeList() { mapper.createArrayNode().add(BIDDER), CookieSyncRequest.FilterType.exclude), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -111,7 +111,7 @@ public void shouldReturnSecondaryMethodWhenMethodFilterExcludesAll() { new TextNode("*"), CookieSyncRequest.FilterType.exclude), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -180,7 +180,7 @@ public void shouldReturnSecondaryMethodWhenNotInMethodFilterIncludeList() { mapper.createArrayNode().add("anotherbidder"), CookieSyncRequest.FilterType.include), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -231,7 +231,7 @@ public void shouldReturnSecondaryMethodWhenMethodFilterIncludeListIsNotArray() { new IntNode(1), CookieSyncRequest.FilterType.include), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter).choose(usersyncer, BIDDER); @@ -248,7 +248,7 @@ public void shouldReturnSecondaryMethodWhenMethodFilterIncludeListIsNotStringArr mapper.createArrayNode().add(1), CookieSyncRequest.FilterType.include), null); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -268,7 +268,7 @@ public void shouldReturnSecondaryMethodWhenPrimaryIsFilteredOutAndSecondIsNot() CookieSyncRequest.MethodFilter.of( new TextNode("*"), CookieSyncRequest.FilterType.include)); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -288,7 +288,7 @@ public void shouldReturnNullWhenPrimaryAndSecondaryAreFilteredOut() { CookieSyncRequest.MethodFilter.of( new TextNode("*"), CookieSyncRequest.FilterType.exclude)); - final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url")); + final Usersyncer usersyncer = Usersyncer.of(null, iframeMethod("url"), redirectMethod("url"), false, null); // when final UsersyncMethod chosenMethod = UsersyncMethodChooser.from(filter) @@ -329,7 +329,7 @@ public void shouldReturnNullWhenPrimaryIsFilteredOutAndNoSecondary() { } private Usersyncer iframeUsersyncer(String url) { - return Usersyncer.of(null, iframeMethod(url), null); + return Usersyncer.of(null, iframeMethod(url), null, false, null); } private UsersyncMethod iframeMethod(String url) { diff --git a/src/test/java/org/prebid/server/bidder/akcelo/AkceloBidderTest.java b/src/test/java/org/prebid/server/bidder/akcelo/AkceloBidderTest.java new file mode 100644 index 00000000000..fe82dcf4714 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/akcelo/AkceloBidderTest.java @@ -0,0 +1,278 @@ +package org.prebid.server.bidder.akcelo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.proto.openrtb.ext.request.akcelo.ExtImpAkcelo; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; + +public class AkceloBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String BIDDER_NAME = "akcelo"; + + private final AkceloBidder target = new AkceloBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AkceloBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Cannot deserialize value of type"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCorrectlyModifyRequest() { + // given + final ObjectNode extImp1 = givenImpExt(1, "1", 1); + final ObjectNode extImp2 = givenImpExt(2, "2", 2); + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1").ext(extImp1), + imp -> imp.id("imp2").ext(extImp2)) + .toBuilder() + .site(Site.builder().id("siteId").publisher(Publisher.builder().id("pubId").build()).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedExtImp1 = mapper.createObjectNode() + .set(BIDDER_NAME, mapper.valueToTree(extImp1.get("bidder"))); + final ObjectNode expectedExtImp2 = mapper.createObjectNode() + .set(BIDDER_NAME, mapper.valueToTree(extImp2.get("bidder"))); + + final Publisher expectedPublisher = Publisher.builder() + .id("pubId") + .ext(ExtPublisher.of(ExtPublisherPrebid.of("1"))) + .build(); + + final Site expectedSite = Site.builder().id("siteId").publisher(expectedPublisher).build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .satisfies(request -> { + assertThat(request.getSite()).isEqualTo(expectedSite); + assertThat(request.getImp()) + .extracting(Imp::getExt) + .containsExactly(expectedExtImp1, expectedExtImp2); + }); + } + + @Test + public void makeHttpRequestsShouldCreateSitePublisherWhenSiteIsAbsent() { + // given + final ObjectNode extImp1 = givenImpExt(1, "1", 1); + final ObjectNode extImp2 = givenImpExt(2, "2", 2); + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1").ext(extImp1), + imp -> imp.id("imp2").ext(extImp2)) + .toBuilder() + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final Site expectedSite = Site.builder().publisher(Publisher.builder() + .ext(ExtPublisher.of(ExtPublisherPrebid.of("1"))) + .build()).build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .isEqualTo(expectedSite); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenMtypeIsBanner() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1, "video"); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidWhenMtypeIsVideo() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(2, "banner"); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidWhenMtypeIsNative() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4, "audio"); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBidsAndErrorsForMixedValidAndInvalidBids() throws JsonProcessingException { + // given + final Bid validBid = givenBid(1, null); + final Bid invalidBid = givenBid(3, "video"); + final BidderCall httpCall = givenHttpCall(givenBidResponse(validBid, invalidBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("unable to get media type 3")); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(validBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenMtypeIsMissingAndExtTypeIsBanner() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(null, "banner"); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeAndExtTypeAreMissing() throws JsonProcessingException { + // given + final Bid invalidBid = givenBid(null, null); + final BidderCall httpCall = givenHttpCall(givenBidResponse(invalidBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("missing media type for bid bidId")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers).map(AkceloBidderTest::givenImp).toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply( + Imp.builder().id("impId").ext(givenImpExt(1, "3", null))) + .build(); + } + + private static ObjectNode givenImpExt(Integer adUnitId, String siteId, Integer test) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpAkcelo.of(adUnitId, siteId, test))); + } + + private static Bid givenBid(Integer mtype, String extType) { + final ObjectNode ext = extType != null + ? mapper.valueToTree(ExtPrebid.of(mapper.createObjectNode().put("type", extType), null)) + : null; + + return Bid.builder() + .id("bidId") + .impid("impId") + .price(BigDecimal.ONE) + .mtype(mtype) + .ext(ext) + .build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/mediasquare/MediasquareBidderTest.java b/src/test/java/org/prebid/server/bidder/mediasquare/MediasquareBidderTest.java new file mode 100644 index 00000000000..b0de11b61f0 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/mediasquare/MediasquareBidderTest.java @@ -0,0 +1,415 @@ +package org.prebid.server.bidder.mediasquare; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.mediasquare.request.MediasquareBanner; +import org.prebid.server.bidder.mediasquare.request.MediasquareCode; +import org.prebid.server.bidder.mediasquare.request.MediasquareFloor; +import org.prebid.server.bidder.mediasquare.request.MediasquareGdpr; +import org.prebid.server.bidder.mediasquare.request.MediasquareMediaTypes; +import org.prebid.server.bidder.mediasquare.request.MediasquareRequest; +import org.prebid.server.bidder.mediasquare.request.MediasquareSupport; +import org.prebid.server.bidder.mediasquare.response.MediasquareBid; +import org.prebid.server.bidder.mediasquare.response.MediasquareResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.mediasquare.ExtImpMediasquare; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class MediasquareBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final MediasquareBidder target = new MediasquareBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new MediasquareBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("can not parse imp.ext"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsOnly(Set.of("givenImp1", "givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com"); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectRequestPayload() { + // given + final Device givenDevice = Device.builder().build(); + final App givenApp = App.builder().build(); + final ExtRegsDsa givenDsa = ExtRegsDsa.of(1, 2, 3, Collections.emptyList()); + + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("imp_id_1") + .tagid("tag_id_1") + .bidfloor(BigDecimal.ONE) + .bidfloorcur("USD") + .banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(250).build())) + .build()) + .ext(givenImpExt("owner1", "code1")), + imp -> imp + .id("imp_id_2") + .tagid("tag_id_2") + .bidfloor(BigDecimal.TEN) + .bidfloorcur("EUR") + .banner(null) + .video(Video.builder().w(640).h(480).build()) + .ext(givenImpExt("owner2", "code2")), + imp -> imp + .id("imp_id_3") + .tagid("tag_id_3") + .bidfloor(BigDecimal.valueOf(0.5)) + .bidfloorcur("USD") + .banner(null) + .xNative(Native.builder().request("native_request_str").build()) + .ext(givenImpExt("owner3", "code3"))) + .toBuilder() + .id("request_id") + .test(1) + .user(User.builder().consent("consent_str").build()) + .regs(Regs.builder().gdpr(1).ext(ExtRegs.of(null, null, null, givenDsa)).build()) + .device(givenDevice) + .app(givenApp) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final MediasquareFloor bannerFloor = MediasquareFloor.of(BigDecimal.ONE, "USD"); + final MediasquareFloor videoFloor = MediasquareFloor.of(BigDecimal.TEN, "EUR"); + final MediasquareFloor nativeFloor = MediasquareFloor.of(BigDecimal.valueOf(0.5), "USD"); + + final MediasquareCode expectedCode1 = MediasquareCode.builder() + .adUnit("tag_id_1") + .auctionId("request_id") + .bidId("imp_id_1") + .owner("owner1") + .code("code1") + .mediaTypes(MediasquareMediaTypes.builder() + .banner(MediasquareBanner.of(List.of(List.of(300, 250)))) + .build()) + .floor(Map.of("300x250", bannerFloor)) + .build(); + final MediasquareCode expectedCode2 = MediasquareCode.builder() + .adUnit("tag_id_2") + .auctionId("request_id") + .bidId("imp_id_2") + .owner("owner2") + .code("code2") + .mediaTypes(MediasquareMediaTypes.builder() + .video(Video.builder().w(640).h(480).build()) + .build()) + .floor(Map.of("640x480", videoFloor, "*", videoFloor)) + .build(); + final MediasquareCode expectedCode3 = MediasquareCode.builder() + .adUnit("tag_id_3") + .auctionId("request_id") + .bidId("imp_id_3") + .owner("owner3") + .code("code3") + .mediaTypes(MediasquareMediaTypes.builder() + .nativeRequest("native_request_str") + .build()) + .floor(Map.of("*", nativeFloor)) + .build(); + + final MediasquareRequest expectedRequest = MediasquareRequest.builder() + .codes(List.of(expectedCode1, expectedCode2, expectedCode3)) + .dsa(givenDsa) + .gdpr(MediasquareGdpr.of(true, "consent_str")) + .type("pbs") + .support(MediasquareSupport.of(givenDevice, givenApp)) + .test(true) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsExactly(expectedRequest); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith( + "Failed to decode response: Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final MediasquareBid mediasquareBid = givenMediasquareBid(1); + final BidderCall httpCall = givenHttpCall( + mapper.writeValueAsString(MediasquareResponse.of(List.of(mediasquareBid)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final ObjectNode expectedExt = mapper.createObjectNode() + .set("prebid", mapper.valueToTree(ExtBidPrebid.builder().meta(ExtBidPrebidMeta.builder() + .mediaType("banner") + .advertiserDomains(List.of("adomain.com")) + .build()).build())); + expectedExt.set("dsa", mapper.createObjectNode().put("key", "value")); + + final Bid expectedBid = Bid.builder() + .id("bidId") + .impid("impId") + .price(BigDecimal.valueOf(1.23)) + .adm("ad-markup") + .adomain(List.of("adomain.com")) + .w(300) + .h(250) + .crid("crid") + .mtype(1) + .burl("burl") + .ext(expectedExt) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(expectedBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException { + // given + final MediasquareBid mediasquareBid = givenMediasquareBid(2); + final BidderCall httpCall = givenHttpCall( + mapper.writeValueAsString(MediasquareResponse.of(List.of(mediasquareBid)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final ObjectNode expectedExt = mapper.createObjectNode() + .set("prebid", mapper.valueToTree(ExtBidPrebid.builder().meta(ExtBidPrebidMeta.builder() + .mediaType("video") + .advertiserDomains(List.of("adomain.com")) + .build()).build())); + expectedExt.set("dsa", mapper.createObjectNode().put("key", "value")); + + final Bid expectedBid = Bid.builder() + .id("bidId") + .impid("impId") + .price(BigDecimal.valueOf(1.23)) + .adm("ad-markup") + .adomain(List.of("adomain.com")) + .w(300) + .h(250) + .crid("crid") + .mtype(2) + .burl("burl") + .ext(expectedExt) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(expectedBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidSuccessfully() throws JsonProcessingException { + // given + final MediasquareBid mediasquareBid = givenMediasquareBid(4); + final BidderCall httpCall = givenHttpCall( + mapper.writeValueAsString(MediasquareResponse.of(List.of(mediasquareBid)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + final ObjectNode expectedExt = mapper.createObjectNode() + .set("prebid", mapper.valueToTree(ExtBidPrebid.builder().meta(ExtBidPrebidMeta.builder() + .mediaType("native") + .advertiserDomains(List.of("adomain.com")) + .build()).build())); + expectedExt.set("dsa", mapper.createObjectNode().put("key", "value")); + + final Bid expectedBid = Bid.builder() + .id("bidId") + .impid("impId") + .price(BigDecimal.valueOf(1.23)) + .adm("ad-markup") + .adomain(List.of("adomain.com")) + .w(300) + .h(250) + .crid("crid") + .mtype(4) + .burl("burl") + .ext(expectedExt) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(expectedBid, BidType.xNative, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(MediasquareBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("imp_id") + .tagid("tag_id") + .bidfloor(BigDecimal.ONE) + .bidfloorcur("USD") + .banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(250).build())) + .build()) + .ext(givenImpExt("owner", "code"))) + .build(); + } + + private static ObjectNode givenImpExt(String owner, String code) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpMediasquare.of(owner, code))); + } + + private static MediasquareBid givenMediasquareBid(Integer mtype) { + final MediasquareBid.MediasquareBidBuilder builder = MediasquareBid.builder() + .id("bidId") + .bidId("impId") + .cpm(BigDecimal.valueOf(1.23)) + .ad("ad-markup") + .adomain(List.of("adomain.com")) + .width(300) + .height(250) + .creativeId("crid") + .burl("burl") + .dsa(mapper.createObjectNode().put("key", "value")) + .currency("USD"); + + if (mtype == 2) { + builder.video(mapper.createObjectNode()); + } else if (mtype == 4) { + builder.nativeResponse(mapper.createObjectNode()); + } + return builder.build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java index 453fcd26739..e7a636fc4bb 100644 --- a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java @@ -129,22 +129,15 @@ public void makeHttpRequestsShouldMakeRequestForFirstValidImp() { final MissenaAdRequest expectedPayload = MissenaAdRequest.builder() .adUnit("impId1") - .buyerUid("buyer1") - .coppa(null) .currency("USD") - .userEids(null) .floor(BigDecimal.valueOf(0.1)) .floorCurrency("USD") - .gdpr(true) - .gdprConsent("consentStr") .idempotencyKey("requestId") - .referer("http://test.com/page") - .refererCanonical("test.com") .requestId("requestId") - .schain(SupplyChain.of(1, null, null, null)) .timeout(500L) .params(expectedUserParams) .version(TEST_PBS_VERSION) + .bidRequest(bidRequest) .build(); assertThat(result.getErrors()).isEmpty(); diff --git a/src/test/java/org/prebid/server/bidder/nexx360/Nexx360BidderTest.java b/src/test/java/org/prebid/server/bidder/nexx360/Nexx360BidderTest.java new file mode 100644 index 00000000000..09c50589e19 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/nexx360/Nexx360BidderTest.java @@ -0,0 +1,286 @@ +package org.prebid.server.bidder.nexx360; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.nexx360.ExtImpNexx360; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.version.PrebidVersionProvider; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class Nexx360BidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String PBS_VERSION = "pbs-java/1.0"; + + @Mock(strictness = LENIENT) + private PrebidVersionProvider prebidVersionProvider; + + private Nexx360Bidder target; + + @BeforeEach + public void setUp() { + target = new Nexx360Bidder(ENDPOINT_URL, jacksonMapper, prebidVersionProvider); + given(prebidVersionProvider.getNameVersionRecord()).willReturn(PBS_VERSION); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new Nexx360Bidder("invalid_url", jacksonMapper, prebidVersionProvider)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Cannot deserialize value of type"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateSingleRequest() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1"), + imp -> imp.id("imp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldSetCorrectUrl() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("tag", "placement"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?placement=placement&tag_id=tag"); + } + + @Test + public void makeHttpRequestsShouldModifyImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1").ext(givenImpExt("tag1", "p1")), + imp -> imp.id("imp2").ext(givenImpExt("tag2", "p2"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedExt1 = mapper.createObjectNode() + .set("nexx360", mapper.valueToTree(ExtImpNexx360.of("tag1", "p1"))); + final ObjectNode expectedExt2 = mapper.createObjectNode() + .set("nexx360", mapper.valueToTree(ExtImpNexx360.of("tag2", "p2"))); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedExt1, expectedExt2); + } + + @Test + public void makeHttpRequestsShouldModifyRequestExt() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ExtRequest expectedExtRequest = ExtRequest.empty(); + expectedExtRequest.addProperty("nexx360", mapper.valueToTree( + Nexx360ExtRequest.of(Nexx360ExtRequestCaller.of("pbs-java/1.0")))); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(req -> req.getPayload().getExt()) + .containsExactly(expectedExtRequest); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidTypeIsUnknown() throws JsonProcessingException { + // given + final Bid bid = givenBid(builder -> builder.ext(givenBidExt("unknown"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("unable to fetch mediaType in multi-format: impId")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(builder -> builder.ext(givenBidExt("banner"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(builder -> builder.ext(givenBidExt("video"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid audioBid = givenBid(builder -> builder.ext(givenBidExt("audio"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(audioBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(audioBid, BidType.audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidTypeIfPresentInBidExt() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(builder -> builder.ext(givenBidExt("native"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(Nexx360BidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(givenImpExt("tagId", "placementId"))) + .build(); + } + + private static ObjectNode givenImpExt(String tagId, String placement) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpNexx360.of(tagId, placement))); + } + + private static Bid givenBid(UnaryOperator bidCustomizer) { + return bidCustomizer.apply(Bid.builder().id("bidId").impid("impId").price(BigDecimal.ONE)).build(); + } + + private static ObjectNode givenBidExt(String bidType) { + return mapper.valueToTree(Nexx360ExtBid.of(bidType)); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/optidigital/OptidigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/optidigital/OptidigitalBidderTest.java new file mode 100644 index 00000000000..a2723b65c80 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/optidigital/OptidigitalBidderTest.java @@ -0,0 +1,153 @@ +package org.prebid.server.bidder.optidigital; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class OptidigitalBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.com"; + + private final OptidigitalBidder target = new OptidigitalBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new OptidigitalBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldNotModifyIncomingRequest() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result; + result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .containsExactly(bidRequest); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBidderBidWithNoErrors() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + final BidderCall httpCall = givenHttpCall(bidRequest, + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("impId")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").build(), BidType.banner, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("requestId") + .imp(Collections.singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .banner(Banner.builder().build()) + .ext(null)) + .build(); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .seatbid(Collections.singletonList(SeatBid.builder() + .bid(Collections.singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .cur("USD") + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java index b44b7d4768d..054d2f2d13e 100644 --- a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java @@ -41,6 +41,7 @@ import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic; import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.ExtIgi; import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; @@ -59,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @@ -1086,48 +1088,101 @@ public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsNull() public void makeBidderResponseShouldReturnBannerBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(1)))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "banner"))); + + assertThat(result.getBids()).containsExactly(BidderBid.of( + Bid.builder().impid("123").mtype(1).ext(expectedBidExt).build(), banner, "USD")); } @Test - public void makeBidderResponseShouldReturnVideoBidIfExtBidContainsBidTypeOne() throws JsonProcessingException { + public void makeBidderResponseShouldReturnVideoBidWhenInBannerVideoIsTrue() throws JsonProcessingException { + // given + final ObjectNode givenBidExt = mapper.createObjectNode().put("ibv", true); + final BidderCall httpCall = givenHttpCall( + mapper.writeValueAsString(givenBidResponse( + bidBuilder -> bidBuilder.impid("123").mtype(1).ext(givenBidExt)))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + + final ObjectNode expectedBidExt = mapper.createObjectNode() + .put("ibv", true) + .set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "video"))); + + assertThat(result.getBids()).containsExactly(BidderBid.of( + Bid.builder().impid("123").mtype(1).ext(expectedBidExt).build(), banner, "USD")); + } + + @Test + public void makeBidderResponseShouldReturnVideoBidIfExtBidContainsMtypeTwo() throws JsonProcessingException { // given - final ObjectNode bidType = mapper.createObjectNode().put("BidType", 1); final BidderCall httpCall = givenHttpCall( mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123").ext(bidType)))); + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(2)))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(bidType).build(), video, "USD")); + + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "video"))); + + assertThat(result.getBids()).containsExactly( + BidderBid.of(Bid.builder().impid("123").mtype(2).ext(expectedBidExt).build(), video, "USD")); } @Test - public void makeBidderResponseShouldReturnXNativeBidIfExtBidContainsBidTypeTwo() throws JsonProcessingException { + public void makeBidderResponseShouldReturnAudioBidIfExtBidContainsMtype3() throws JsonProcessingException { // given - final ObjectNode bidType = mapper.createObjectNode().put("BidType", 2); final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").ext(bidType)))); + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(3)))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(bidType).build(), xNative, "USD")); + + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "audio"))); + + assertThat(result.getBids()).containsExactly( + BidderBid.of(Bid.builder().impid("123").mtype(3).ext(expectedBidExt).build(), audio, "USD")); + } + + @Test + public void makeBidderResponseShouldReturnXNativeBidIfExtBidContainsMtypeFour() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(4)))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "native"))); + + assertThat(result.getBids()).containsExactly( + BidderBid.of(Bid.builder().impid("123").mtype(4).ext(expectedBidExt).build(), xNative, "USD")); } @Test @@ -1136,9 +1191,9 @@ public void makeBidderResponseShouldFillExtBidPrebidVideoDurationIfDurationIsNot // given final BidderCall httpCall = givenHttpCall( mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123") + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(2) .ext(mapper.valueToTree( - PubmaticBidExt.of(null, VideoCreativeInfo.of(1), null, null)))))); + PubmaticBidExt.of(VideoCreativeInfo.of(1), null, null, null)))))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -1148,9 +1203,12 @@ public void makeBidderResponseShouldFillExtBidPrebidVideoDurationIfDurationIsNot final ObjectNode bidExt = mapper.createObjectNode(); bidExt.set("video", mapper.valueToTree(VideoCreativeInfo.of(1))); - bidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().video(ExtBidPrebidVideo.of(1, null)).build())); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(bidExt).build(), banner, "USD")); + bidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .video(ExtBidPrebidVideo.of(1, null)) + .build())); + assertThat(result.getBids()).containsExactly( + BidderBid.of(Bid.builder().mtype(2).impid("123").ext(bidExt).build(), video, "USD")); } @Test @@ -1161,7 +1219,7 @@ public void makeBidderResponseShouldNotFillExtBidPrebidVideoDurationIfDurationIs mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123") .ext(mapper.valueToTree( - PubmaticBidExt.of(null, VideoCreativeInfo.of(null), null, null)))))); + PubmaticBidExt.of(VideoCreativeInfo.of(null), null, null, null)))))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -1170,9 +1228,12 @@ public void makeBidderResponseShouldNotFillExtBidPrebidVideoDurationIfDurationIs assertThat(result.getErrors()).isEmpty(); final ObjectNode bidExt = mapper.createObjectNode(); bidExt.set("video", mapper.valueToTree(VideoCreativeInfo.of(null))); + bidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("banner").build()) + .build())); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(bidExt).build(), banner, "USD")); + assertThat(result.getBids()).containsExactly( + BidderBid.of(Bid.builder().impid("123").mtype(1).ext(bidExt).build(), banner, "USD")); } @Test @@ -1189,8 +1250,11 @@ public void makeBidderResponseShouldNotFillExtBidPrebidVideoDurationIfVideoIsNul // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()).containsExactly( - BidderBid.of(Bid.builder().impid("123").ext(mapper.createObjectNode()).build(), banner, "USD")); + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "banner"))); + + assertThat(result.getBids()).containsExactly(BidderBid.of( + Bid.builder().impid("123").mtype(1).ext(expectedBidExt).build(), banner, "USD")); } @Test @@ -1200,7 +1264,7 @@ public void makeBidderResponseShouldFillDealPriorityData() throws JsonProcessing mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123") .ext(mapper.valueToTree( - PubmaticBidExt.of(null, null, 12, null)))))); + PubmaticBidExt.of(null, 12, null, null)))))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -1219,7 +1283,7 @@ public void makeBidderResponseShouldFillSeat() throws JsonProcessingException { mapper.writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123") .ext(mapper.valueToTree( - PubmaticBidExt.of(null, null, 12, "marketplace")))))); + PubmaticBidExt.of(null, 12, "marketplace", null)))))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -1240,10 +1304,10 @@ public void makeBidderResponseShouldParseNativeAdmData() throws JsonProcessingEx admNode.set("native", nativeNode); final BidderCall httpCall = givenHttpCall( mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123") + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(4) .adm(admNode.toString()) .ext(mapper.valueToTree( - PubmaticBidExt.of(2, null, 12, null)))))); + PubmaticBidExt.of(null, 12, null, null)))))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -1269,24 +1333,32 @@ public void makeBidderResponseShouldTakeOnlyFirstCatElement() throws JsonProcess // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()).containsExactly( - BidderBid.of(Bid.builder().impid("123").cat(singletonList("cat1")).build(), banner, "USD")); + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "banner"))); + + assertThat(result.getBids()).containsExactly(BidderBid.of( + Bid.builder() + .impid("123") + .mtype(1) + .cat(singletonList("cat1")) + .ext(expectedBidExt).build(), + banner, + "USD")); } @Test public void makeBidderResponseShouldReturnBannerBidIfExtBidContainsIllegalBidType() throws JsonProcessingException { // given - final ObjectNode bidType = mapper.createObjectNode().put("BidType", 100); final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").ext(bidType)))); + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(100)))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").ext(bidType).build(), banner, "USD")); + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse("failed to parse bid mtype (100) for impression id 123")); + assertThat(result.getBids()).isEmpty(); } @Test @@ -1306,8 +1378,11 @@ public void makeBidderResponseShouldReturnFledgeAuctionConfig() throws JsonProce // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getBids()) - .containsExactly(BidderBid.of(Bid.builder().impid("imp_id").build(), banner, "USD")); + final ObjectNode expectedBidExt = mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("mediaType", "banner"))); + + assertThat(result.getBids()).containsExactly(BidderBid.of( + Bid.builder().impid("imp_id").mtype(1).ext(expectedBidExt).build(), banner, "USD")); final ExtIgi igi = ExtIgi.builder() .igs(singletonList(ExtIgiIgs.builder().impId("imp_id").config(auctionConfig).build())) @@ -1325,7 +1400,7 @@ public void makeBidderResponseShouldNotModifyAdmWhenNativeNodeIsNull() throws Js givenBidResponse(bidBuilder -> bidBuilder.impid("123") .adm(admNode.toString()) .ext(mapper.valueToTree( - PubmaticBidExt.of(2, null, 12, null)))))); + PubmaticBidExt.of(null, 12, null, null)))))); // when final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); @@ -1413,7 +1488,7 @@ private static BidResponse givenBidResponse(UnaryOperator bidCus return BidResponse.builder() .cur("USD") .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .bid(singletonList(bidCustomizer.apply(Bid.builder().mtype(1)).build())) .build())) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/rediads/RediadsBidderTest.java b/src/test/java/org/prebid/server/bidder/rediads/RediadsBidderTest.java new file mode 100644 index 00000000000..4d294c78856 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/rediads/RediadsBidderTest.java @@ -0,0 +1,276 @@ +package org.prebid.server.bidder.rediads; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.rediads.ExtImpRediads; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.groups.Tuple.tuple; +import static org.prebid.server.bidder.model.BidderError.Type; +import static org.prebid.server.bidder.model.BidderError.badInput; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; + +public class RediadsBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "http://{{SUBDOMAIN}}.domain.com/path"; + + private final RediadsBidder target = new RediadsBidder(ENDPOINT_URL, jacksonMapper, "subdomain"); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new RediadsBidder("invalid_url", jacksonMapper, "subdomain")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly(badInput("Invalid imp.ext for impression impId")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldModifyImpCorrectly() { + // given + final ObjectNode impExt = givenImpExt("accId", "slotId", "endpointId"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt).tagid("tagId")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid, Imp::getExt) + .containsOnly(tuple("slotId", mapper.createObjectNode())); + } + + @Test + public void makeHttpRequestsShouldModifySiteCorrectly() { + // given + final ObjectNode impExt = givenImpExt("accId", "slotId", "endpointId"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt).tagid("tagId")) + .toBuilder() + .site(Site.builder().publisher(Publisher.builder().id("pubId1").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("pubId2").build()).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite, BidRequest::getApp) + .containsOnly(tuple( + Site.builder().publisher(Publisher.builder().id("accId").build()).build(), + bidRequest.getApp())); + } + + @Test + public void makeHttpRequestsShouldModifyAppCorrectlyWhenSiteIsNull() { + // given + final ObjectNode impExt = givenImpExt("accId", "slotId", "endpointId"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt).tagid("tagId")) + .toBuilder() + .app(App.builder().publisher(Publisher.builder().id("pubId2").build()).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getApp) + .containsOnly(App.builder().publisher(Publisher.builder().id("accId").build()).build()); + } + + @Test + public void makeHttpRequestsShouldResolveEndpointUrlCorrectly() { + // given + final ObjectNode impExt = givenImpExt("accId", "slotId", "newsub"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("http://newsub.domain.com/path"); + } + + @Test + public void makeHttpRequestsShouldUseOriginalEndpointWhenEndpointInExtIsBlank() { + // given + final ObjectNode impExt = givenImpExt("accId", "slotId", null); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(impExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("http://subdomain.domain.com/path"); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeIsUnsupported() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().impid("impId").mtype(5).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("could not define media type for impression: impId")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().mtype(1).price(BigDecimal.ONE).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().mtype(2).price(BigDecimal.TEN).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBid() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().mtype(3).price(BigDecimal.TEN).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid, BidType.audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().mtype(4).price(BigDecimal.TEN).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(bid, BidType.xNative, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static ObjectNode givenImpExt(String accountId, String slot, String endpoint) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpRediads.of(accountId, slot, endpoint))); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidderTest.java b/src/test/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidderTest.java index 11e789e8a24..aa918224d29 100644 --- a/src/test/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidderTest.java @@ -2,20 +2,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Imp.ImpBuilder; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import io.vertx.core.http.HttpMethod; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -25,8 +19,6 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.stroeercore.model.StroeerCoreBid; import org.prebid.server.bidder.stroeercore.model.StroeerCoreBidResponse; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; @@ -46,25 +38,12 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) public class StroeerCoreBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; - @Mock - private CurrencyConversionService currencyConversionService; - - private StroeerCoreBidder target; - - @BeforeEach - public void setUp() { - target = new StroeerCoreBidder(ENDPOINT_URL, jacksonMapper, currencyConversionService); - } + private final StroeerCoreBidder target = new StroeerCoreBidder(ENDPOINT_URL, jacksonMapper); @Test public void makeHttpRequestsShouldReturnExpectedMethod() { @@ -171,146 +150,28 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldReturnErrorWhenImpHasNoBannerOrVideo() { - // given - final BidRequest invalidBidRequest = createBidRequest(createAudioImp("123", imp -> imp.id("2"))); - - // when - final Result>> result = target.makeHttpRequests(invalidBidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .containsExactly(BidderError.badInput("Expected banner or video impression. Ignore imp id = 2.")); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfSlotIdIsEmpty() { - // given - final BidRequest invalidBidRequest = createBidRequest(createBannerImp(" ", imp -> imp.id("1"))); - - // when - final Result>> result = target.makeHttpRequests(invalidBidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()) - .containsExactly(BidderError.badInput("Custom param slot id (sid) is empty. Ignore imp id = 1.")); - } - - @Test - public void makeHttpRequestsShouldIgnoreInvalidImpressions() { - // given - final List imps = List.of( - createImpWithNonParsableImpExt("2"), - createBannerImp(" "), - createBannerImp("a"), - createBannerImp("b", imp -> imp.banner(null)), - createAudioImp("not-supported", identity()), - createVideoImp("c"), - createBannerImp("d"), - createBannerImp("e", imp -> imp.bidfloor(BigDecimal.ONE).bidfloorcur("GPB"))); - - when(currencyConversionService.convertCurrency(any(), any(), any(), any())) - .thenThrow(new PreBidException("no")); - - final BidRequest bidRequest = BidRequest.builder().imp(imps).build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(5); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getTagid) - .containsExactly("a", "c", "d"); - } - - @Test - public void makeHttpRequestsShouldConvertCurrencyToEuro() { - // given - final BigDecimal usdBidFloor = BigDecimal.valueOf(0.5); - final Imp usdImp = createBannerImp("200", imp -> imp.bidfloorcur("USD").bidfloor(usdBidFloor)); - final BidRequest bidRequest = createBidRequest(usdImp); - - final BigDecimal eurBidFloor = BigDecimal.valueOf(1.82); - when(currencyConversionService.convertCurrency(any(), any(), any(), any())).thenReturn(eurBidFloor); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - verify(currencyConversionService).convertCurrency(usdBidFloor, bidRequest, "USD", "EUR"); - verifyNoMoreInteractions(currencyConversionService); - - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getBidfloor, Imp::getBidfloorcur) - .containsOnly(tuple(eurBidFloor, "EUR")); - } - - @Test - public void makeHttpRequestsShouldIgnoreBidIfCurrencyServiceThrowsException() { + public void makeBidsShouldReturnExpectedBannerBid() throws JsonProcessingException { // given - final BigDecimal usdBidFloor = BigDecimal.valueOf(0.5); - final Imp usdImp = createBannerImp("10", imp -> imp.id("1282").bidfloorcur("USD").bidfloor(usdBidFloor)); - final BidRequest bidRequest = createBidRequest(usdImp); - - when(currencyConversionService.convertCurrency(any(), any(), any(), any())) - .thenThrow(new PreBidException("no")); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - verify(currencyConversionService).convertCurrency(usdBidFloor, bidRequest, "USD", "EUR"); - verifyNoMoreInteractions(currencyConversionService); - - assertThat(result.getErrors()).allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); - assertThat(error.getMessage()).startsWith("no. Ignore imp id = 1282."); - }); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeBidsShouldReturnExpectedBidderBids() throws JsonProcessingException { - // given - final Imp bannerImp = createBannerImp("banner-slot-id", impBuilder -> impBuilder.id("banner-imp-id")); - final Imp videoImp = createVideoImp("video-slot-id", impBuilder -> impBuilder.id("video-imp-id")); - final BidRequest bidRequest = createBidRequest(bannerImp, videoImp); - final ObjectNode dsaResponse = createDsaResponse(); final StroeerCoreBid bannerBid = StroeerCoreBid.builder() .id("1") - .impId("banner-imp-id") + .bidId("banner-imp-id") .adMarkup("
") .cpm(BigDecimal.valueOf(0.3)) .creativeId("foo") .width(300) .height(600) + .mtype("banner") .dsa(dsaResponse.deepCopy()) + .adomain(List.of("domain1.com", "domain2.com")) .build(); - final StroeerCoreBid videoBid = StroeerCoreBid.builder() - .id("27") - .impId("video-imp-id") - .adMarkup("") - .cpm(BigDecimal.valueOf(1.58)) - .creativeId("vid") - .dsa(null) - .build(); - - final StroeerCoreBidResponse response = StroeerCoreBidResponse.of(List.of(bannerBid, videoBid)); - final BidderCall httpCall = createHttpCall(bidRequest, response); + final StroeerCoreBidResponse response = StroeerCoreBidResponse.of(List.of(bannerBid)); + final BidderCall httpCall = createHttpCall(response); // when - final Result> result = target.makeBids(httpCall, bidRequest); + final Result> result = target.makeBids(httpCall, null); // then final Bid expectedBannerBid = Bid.builder() @@ -321,21 +182,68 @@ public void makeBidsShouldReturnExpectedBidderBids() throws JsonProcessingExcept .crid("foo") .w(300) .h(600) + .adomain(List.of("domain1.com", "domain2.com")) + .mtype(1) .ext(mapper.createObjectNode().set("dsa", dsaResponse)) .build(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(expectedBannerBid, BidType.banner, "EUR")); + } + + @Test + public void makeBidsShouldReturnExpectedBidderBids() throws JsonProcessingException { + // given + final StroeerCoreBid videoBid = StroeerCoreBid.builder() + .id("27") + .bidId("video-imp-id") + .adMarkup("") + .cpm(BigDecimal.valueOf(1.58)) + .creativeId("vid") + .mtype("video") + .dsa(null) + .build(); + + final StroeerCoreBidResponse response = StroeerCoreBidResponse.of(List.of(videoBid)); + final BidderCall httpCall = createHttpCall(response); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then final Bid expectedVideoBid = Bid.builder() .id("27") .impid("video-imp-id") .adm("") .price(BigDecimal.valueOf(1.58)) .crid("vid") + .mtype(2) .ext(null) .build(); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).containsOnly(BidderBid.of(expectedBannerBid, BidType.banner, "EUR"), - BidderBid.of(expectedVideoBid, BidType.video, "EUR")); + assertThat(result.getValue()).containsOnly(BidderBid.of(expectedVideoBid, BidType.video, "EUR")); + } + + @Test + public void makeBidsShouldFailWhenBidTypeIsNotSupported() throws JsonProcessingException { + // given + final StroeerCoreBid audioBid = StroeerCoreBid.builder() + .id("27") + .bidId("audio-imp-id") + .mtype("audio") + .build(); + + final StroeerCoreBidResponse response = StroeerCoreBidResponse.of(List.of(audioBid)); + final BidderCall httpCall = createHttpCall(response); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse( + "Bid media type error: unable to determine media type for bid with id \"audio-imp-id\"")); + assertThat(result.getValue()).isEmpty(); } @Test @@ -357,8 +265,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { @Test public void makeBidsShouldReturnEmptyListIfZeroBids() throws JsonProcessingException { // given - final BidderCall httpCall = createHttpCall(BidRequest.builder().build(), - StroeerCoreBidResponse.of(emptyList())); + final BidderCall httpCall = createHttpCall(StroeerCoreBidResponse.of(emptyList())); // when final Result> result = target.makeBids(httpCall, null); @@ -369,9 +276,7 @@ public void makeBidsShouldReturnEmptyListIfZeroBids() throws JsonProcessingExcep } private BidRequest createBidRequest(Imp... imps) { - return BidRequest.builder() - .imp(List.of(imps)) - .build(); + return BidRequest.builder().imp(List.of(imps)).build(); } private Imp createImpWithNonParsableImpExt(String impId) { @@ -388,20 +293,6 @@ private Imp createBannerImp(String slotId) { return createBannerImp(slotId, identity()); } - private Imp createVideoImp(String slotId, UnaryOperator impCustomizer) { - final UnaryOperator addVideo = imp -> imp.video(Video.builder().build()); - return createImp(slotId, addVideo.andThen(impCustomizer)); - } - - private Imp createVideoImp(String slotId) { - return createVideoImp(slotId, identity()); - } - - private Imp createAudioImp(String slotId, UnaryOperator impCustomizer) { - final UnaryOperator addAudio = imp -> imp.audio(Audio.builder().build()); - return createImp(slotId, addAudio.andThen(impCustomizer)); - } - private Imp createImp(String slotId, Function impCustomizer) { final ObjectNode impExtNode = mapper.valueToTree(ExtPrebid.of(null, ExtImpStroeerCore.of(slotId))); @@ -411,9 +302,8 @@ private Imp createImp(String slotId, Function impCustomi return addImpExt.andThen(impCustomizer).apply(impBuilder).build(); } - private BidderCall createHttpCall(BidRequest request, StroeerCoreBidResponse response) - throws JsonProcessingException { - return createHttpCall(HttpRequest.builder().payload(request).build(), + private BidderCall createHttpCall(StroeerCoreBidResponse response) throws JsonProcessingException { + return createHttpCall(HttpRequest.builder().build(), HttpResponse.of(200, null, mapper.writeValueAsString(response))); } diff --git a/src/test/java/org/prebid/server/bidder/zentotem/ZentotemBidderTest.java b/src/test/java/org/prebid/server/bidder/zentotem/ZentotemBidderTest.java new file mode 100644 index 00000000000..b17b44699c1 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/zentotem/ZentotemBidderTest.java @@ -0,0 +1,197 @@ +package org.prebid.server.bidder.zentotem; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; + +public class ZentotemBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final ZentotemBidder target = new ZentotemBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new ZentotemBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateSeparateRequestForEachImp() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1"), + imp -> imp.id("imp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactly(Set.of("imp1"), Set.of("imp2")); + } + + @Test + public void makeHttpRequestsShouldSetCorrectUriAndBody() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("imp1")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final BidRequest expectedRequest = bidRequest.toBuilder() + .imp(singletonList(bidRequest.getImp().getFirst())) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .satisfies(httpRequest -> { + assertThat(httpRequest.getUri()).isEqualTo(ENDPOINT_URL); + assertThat(httpRequest.getPayload()).isEqualTo(expectedRequest); + assertThat(httpRequest.getBody()).isEqualTo(jacksonMapper.encodeToBytes(expectedRequest)); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListWhenBidResponseOrSeatBidAreNull() throws JsonProcessingException { + // given + final BidResponse bidResponseWithNullSeatBid = BidResponse.builder().seatbid(null).build(); + final BidderCall httpCallWithNullSeatBid = + givenHttpCall(mapper.writeValueAsString(bidResponseWithNullSeatBid)); + + // when + final Result> nullSeatBidResult = target.makeBids(httpCallWithNullSeatBid, null); + + // then + assertThat(nullSeatBidResult.getErrors()).isEmpty(); + assertThat(nullSeatBidResult.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(2); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException { + // given + final Bid bidWithUnsupportedMtype = givenBid(3); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bidWithUnsupportedMtype)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("could not define media type for impression: impId")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(ZentotemBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static Bid givenBid(Integer mtype) { + return Bid.builder().id("bidId").impid("impId").price(BigDecimal.ONE).mtype(mtype).build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java b/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java index 2bb02ee4463..bafc6a11d16 100644 --- a/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java +++ b/src/test/java/org/prebid/server/cookie/CookieSyncServiceTest.java @@ -47,6 +47,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @@ -404,7 +405,9 @@ public void processContextShouldRejectBiddersWithDisabledUsersync() { false, "bidder-with-disabled-usersync", "bidder-with-disabled-usersync-cookie-family", - CookieFamilySource.ROOT); + CookieFamilySource.ROOT, + false, + null); givenAllAllowedTcfResultForBidders("requested-bidder", "coop-sync-bidder", "bidder-with-disabled-usersync"); @@ -422,6 +425,124 @@ public void processContextShouldRejectBiddersWithDisabledUsersync() { .isEqualTo(Map.of("bidder-with-disabled-usersync", RejectionReason.DISABLED_USERSYNC)); } + @Test + public void processContextShouldRejectBiddersWhenUserIsGdprScope() { + // given + givenCoopSyncBidders("coop-sync-bidder"); + + givenValidActiveBidders("requested-bidder", "coop-sync-bidder", "bidder-skipwhen-in-gdpr-scope"); + givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + givenUsersyncerForBidder( + true, + "bidder-skipwhen-in-gdpr-scope", + "bidder-skipwhen-in-gdpr-scope-cookie-family", + CookieFamilySource.ROOT, + true, + null); + + givenAllAllowedTcfResultForBidders("requested-bidder", "coop-sync-bidder", "bidder-skipwhen-in-gdpr-scope"); + + final TcfContext givenTcfContext = TcfContext.builder() + .inGdprScope(true) + .consentValid(true) + .consentString("consent-string") + .build(); + + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(builder -> builder + .privacyContext(givenPrivacyContext(givenTcfContext)) + .cookieSyncRequest(givenCookieSyncRequest("requested-bidder", "bidder-skipwhen-in-gdpr-scope"))); + + // when + final Future result = target.processContext(cookieSyncContext); + + // then + assertThat(result).isSucceeded() + .unwrap() + .extracting(CookieSyncContext::getBiddersContext) + .extracting(BiddersContext::rejectedBidders) + .isEqualTo(Map.of("bidder-skipwhen-in-gdpr-scope", RejectionReason.REJECTED_BY_REGULATION_SCOPE)); + } + + @Test + public void processContextShouldRejectBiddersWhenBidderShouldBeSkippedForSid() { + // given + givenCoopSyncBidders("coop-sync-bidder"); + + givenValidActiveBidders( + "requested-bidder", + "coop-sync-bidder", + "bidder-skipwhen-sid-1", + "bidder-skipwhen-sid-2", + "bidder-skipwhen-sid-1-3", + "bidder-skipwhen-sid-absent"); + + givenUsersyncersForBidders("requested-bidder", "coop-sync-bidder"); + givenUsersyncerForBidder( + true, + "bidder-skipwhen-sid-1", + "bidder-skipwhen-sid-1-cookie-family", + CookieFamilySource.ROOT, + false, + List.of(1)); + + givenUsersyncerForBidder( + true, + "bidder-skipwhen-sid-2", + "bidder-skipwhen-sid-2-cookie-family", + CookieFamilySource.ROOT, + false, + List.of(2)); + + givenUsersyncerForBidder( + true, + "bidder-skipwhen-sid-1-3", + "bidder-skipwhen-sid-1-3-cookie-family", + CookieFamilySource.ROOT, + false, + List.of(1, 3)); + + givenUsersyncerForBidder( + true, + "bidder-skipwhen-sid-absent", + "bidder-skipwhen-sid-absent-cookie-family", + CookieFamilySource.ROOT, + false, + null); + + givenAllAllowedTcfResultForBidders( + "requested-bidder", + "coop-sync-bidder", + "bidder-skipwhen-sid-1", + "bidder-skipwhen-sid-2", + "bidder-skipwhen-sid-1-3", + "bidder-skipwhen-sid-absent"); + + final CookieSyncRequest givenCookieSyncRequest = CookieSyncRequest.builder() + .bidders(Set.of( + "requested-bidder", + "bidder-skipwhen-sid-1", + "bidder-skipwhen-sid-2", + "bidder-skipwhen-sid-1-3", + "bidder-skipwhen-sid-absent")) + .gppSid(List.of(1, 10)) + .build(); + + final CookieSyncContext cookieSyncContext = givenCookieSyncContext(builder -> builder + .cookieSyncRequest(givenCookieSyncRequest)); + + // when + final Future result = target.processContext(cookieSyncContext); + + // then + assertThat(result).isSucceeded() + .unwrap() + .extracting(CookieSyncContext::getBiddersContext) + .extracting(BiddersContext::rejectedBidders) + .isEqualTo(Map.of( + "bidder-skipwhen-sid-1", RejectionReason.REJECTED_BY_REGULATION_SCOPE, + "bidder-skipwhen-sid-1-3", RejectionReason.REJECTED_BY_REGULATION_SCOPE)); + } + @Test public void processContextShouldApplyRequestFilteringRules() { // given @@ -892,7 +1013,7 @@ public void prepareResponseShouldReturnWarningForAliasesSyncedAsRootCookieFamily given(bidderCatalog.isValidName("alias")).willReturn(true); given(bidderCatalog.isActive("alias")).willReturn(true); given(bidderCatalog.isAlias("alias")).willReturn(true); - givenUsersyncerForBidder(true, "alias", "root-cookie-family", CookieFamilySource.ROOT); + givenUsersyncerForBidder(true, "alias", "root-cookie-family", CookieFamilySource.ROOT, false, null); final CookieSyncContext cookieSyncContext = givenCookieSyncContext( cookieSyncContextBuilder -> cookieSyncContextBuilder.debug(true), @@ -920,7 +1041,7 @@ public void prepareResponseShouldNotReturnWarningForAliasesSyncedAsAliasCookieFa given(bidderCatalog.isValidName("alias")).willReturn(true); given(bidderCatalog.isActive("alias")).willReturn(true); given(bidderCatalog.isAlias("alias")).willReturn(true); - givenUsersyncerForBidder(true, "alias", "alias-cookie-family", CookieFamilySource.ALIAS); + givenUsersyncerForBidder(true, "alias", "alias-cookie-family", CookieFamilySource.ALIAS, false, null); final CookieSyncContext cookieSyncContext = givenCookieSyncContext( cookieSyncContextBuilder -> cookieSyncContextBuilder.debug(true), @@ -1128,17 +1249,19 @@ private void givenUsersyncersForBidders(String... bidders) { } private void givenUsersyncerForBidder(String bidder) { - givenUsersyncerForBidder(true, bidder, bidder + "-cookie-family", CookieFamilySource.ROOT); + givenUsersyncerForBidder(true, bidder, bidder + "-cookie-family", CookieFamilySource.ROOT, false, null); } private void givenUsersyncerForBidder(boolean enabled, String bidder, String cookieFamilyName, - CookieFamilySource cookieFamilySource) { + CookieFamilySource cookieFamilySource, + boolean gdpr, + List gppSid) { final UsersyncMethod usersyncMethod = givenUsersyncMethod(bidder); final Usersyncer usersyncer = Usersyncer.of( - enabled, cookieFamilyName, cookieFamilySource, usersyncMethod, null); + enabled, cookieFamilyName, cookieFamilySource, usersyncMethod, null, gdpr, gppSid); given(bidderCatalog.usersyncerByName(eq(bidder))).willReturn(Optional.of(usersyncer)); given(bidderCatalog.cookieFamilyName(eq(bidder))).willReturn(Optional.of(cookieFamilyName)); diff --git a/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java b/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java index 48d5ce91667..49ee89ff6d8 100644 --- a/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java +++ b/src/test/java/org/prebid/server/cookie/CoopSyncProviderTest.java @@ -176,6 +176,8 @@ private void givenValidBidderWithCookieSync(String bidder) { Optional.of(Usersyncer.of( "cookie-family-name", UsersyncMethod.builder().build(), + null, + false, null))); } } diff --git a/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java b/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java index 4964d8ad4fa..a6e24bed906 100644 --- a/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java +++ b/src/test/java/org/prebid/server/cookie/PrioritizedCoopSyncProviderTest.java @@ -106,6 +106,8 @@ private void givenValidBidderWithCookieSync(String bidder) { Optional.of(Usersyncer.of( "cookie-family-name", UsersyncMethod.builder().build(), + null, + false, null))); } } diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index e83b75b83d2..ed88f7f6fdd 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -153,15 +153,15 @@ public void setUp() { given(bidderCatalog.isAlias(any())).willReturn(false); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( - Optional.of(Usersyncer.of(RUBICON, null, redirectMethod()))); + Optional.of(Usersyncer.of(RUBICON, null, redirectMethod(), false, null))); given(bidderCatalog.cookieFamilyName(eq(RUBICON))).willReturn(Optional.of(RUBICON)); given(bidderCatalog.usersyncerByName(eq(FACEBOOK))).willReturn( - Optional.of(Usersyncer.of(FACEBOOK, null, redirectMethod()))); + Optional.of(Usersyncer.of(FACEBOOK, null, redirectMethod(), false, null))); given(bidderCatalog.cookieFamilyName(eq(FACEBOOK))).willReturn(Optional.of(FACEBOOK)); given(bidderCatalog.usersyncerByName(eq(APPNEXUS))).willReturn( - Optional.of(Usersyncer.of(ADNXS, null, redirectMethod()))); + Optional.of(Usersyncer.of(ADNXS, null, redirectMethod(), false, null))); given(bidderCatalog.cookieFamilyName(eq(APPNEXUS))).willReturn(Optional.of(ADNXS)); given(activityInfrastructure.isAllowed(any(), any())) @@ -532,7 +532,7 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() { given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); given(bidderCatalog.usersyncerByName(any())) - .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod()))); + .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod(), false, null))); setuidHandler = new SetuidHandler( 2000, @@ -569,7 +569,7 @@ public void shouldSendEmptyResponseWhenFParamNotDefinedAndTypeIsIframe() { .willReturn(updated(uidsCookie)); given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn( - Optional.of(Usersyncer.of(RUBICON, iframeMethod(), null))); + Optional.of(Usersyncer.of(RUBICON, iframeMethod(), null, false, null))); given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); @@ -611,7 +611,7 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() { given(httpRequest.getParam("bidder")).willReturn(RUBICON); given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON)); given(bidderCatalog.usersyncerByName(any())) - .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod()))); + .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod(), false, null))); given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT"); setuidHandler = new SetuidHandler( @@ -844,11 +844,11 @@ public void shouldThrowExceptionInCaseOfBaseBidderCookieFamilyNameDuplicates() { .willReturn(Set.of(RUBICON, FACEBOOK, firstDuplicateName, secondDuplicateName, thirdDuplicateName)); given(bidderCatalog.isAlias(thirdDuplicateName)).willReturn(true); given(bidderCatalog.usersyncerByName(eq(firstDuplicateName))).willReturn( - Optional.of(Usersyncer.of(RUBICON, iframeMethod(), redirectMethod()))); + Optional.of(Usersyncer.of(RUBICON, iframeMethod(), redirectMethod(), false, null))); given(bidderCatalog.usersyncerByName(eq(secondDuplicateName))).willReturn( - Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod()))); + Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod(), false, null))); given(bidderCatalog.usersyncerByName(eq(thirdDuplicateName))).willReturn( - Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod()))); + Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod(), false, null))); final Executable exceptionSource = () -> new SetuidHandler( 2000, diff --git a/src/test/java/org/prebid/server/it/AkceloTest.java b/src/test/java/org/prebid/server/it/AkceloTest.java new file mode 100644 index 00000000000..464a861d43d --- /dev/null +++ b/src/test/java/org/prebid/server/it/AkceloTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class AkceloTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAkcelo() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/akcelo-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/akcelo/test-akcelo-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/akcelo/test-akcelo-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/akcelo/test-auction-akcelo-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/akcelo/test-auction-akcelo-response.json", response, singletonList("akcelo")); + } +} diff --git a/src/test/java/org/prebid/server/it/AlgorixTest.java b/src/test/java/org/prebid/server/it/AlgorixTest.java index 2c2e95ddd01..9d9dde25bc3 100644 --- a/src/test/java/org/prebid/server/it/AlgorixTest.java +++ b/src/test/java/org/prebid/server/it/AlgorixTest.java @@ -13,9 +13,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static java.util.Collections.singletonList; -/** - * Algorix Test in org.prebid.server.it - */ public class AlgorixTest extends IntegrationTest { @Test diff --git a/src/test/java/org/prebid/server/it/EasybidTest.java b/src/test/java/org/prebid/server/it/EasybidTest.java new file mode 100644 index 00000000000..3993578f48b --- /dev/null +++ b/src/test/java/org/prebid/server/it/EasybidTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class EasybidTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromEasybid() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/easybid-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/easybid/test-easybid-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/easybid/test-easybid-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/easybid/test-auction-easybid-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/easybid/test-auction-easybid-response.json", response, singletonList("easybid")); + } +} diff --git a/src/test/java/org/prebid/server/it/MediasquareTest.java b/src/test/java/org/prebid/server/it/MediasquareTest.java new file mode 100644 index 00000000000..f246637921d --- /dev/null +++ b/src/test/java/org/prebid/server/it/MediasquareTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class MediasquareTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMediasquare() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/mediasquare-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/mediasquare/test-mediasquare-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/mediasquare/test-mediasquare-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/mediasquare/test-auction-mediasquare-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/mediasquare/test-auction-mediasquare-response.json", + response, + List.of("mediasquare")); + } + +} diff --git a/src/test/java/org/prebid/server/it/Nexx360Test.java b/src/test/java/org/prebid/server/it/Nexx360Test.java new file mode 100644 index 00000000000..e32a6a93918 --- /dev/null +++ b/src/test/java/org/prebid/server/it/Nexx360Test.java @@ -0,0 +1,34 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class Nexx360Test extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromNexx360() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/nexx360-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/nexx360/test-nexx360-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/nexx360/test-nexx360-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/nexx360/test-auction-nexx360-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/nexx360/test-auction-nexx360-response.json", response, singletonList("nexx360")); + } +} diff --git a/src/test/java/org/prebid/server/it/OneAccordTest.java b/src/test/java/org/prebid/server/it/OneAccordTest.java new file mode 100644 index 00000000000..bce3a853e14 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OneAccordTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class OneAccordTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOneaccord() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/1accord-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/oneaccord/test-1accord-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/oneaccord/test-1accord-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/oneaccord/test-auction-1accord-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/oneaccord/test-auction-1accord-response.json", response, singletonList("1accord")); + } +} diff --git a/src/test/java/org/prebid/server/it/OptidigitalTest.java b/src/test/java/org/prebid/server/it/OptidigitalTest.java new file mode 100644 index 00000000000..61188100aad --- /dev/null +++ b/src/test/java/org/prebid/server/it/OptidigitalTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class OptidigitalTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOptidigital() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/optidigital-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/optidigital/test-optidigital-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/optidigital/test-optidigital-bid-response.json")))); + + // when + final Response response = + responseFor("openrtb2/optidigital/test-auction-optidigital-request.json", Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/optidigital/test-auction-optidigital-response.json", + response, + singletonList("optidigital")); + } +} diff --git a/src/test/java/org/prebid/server/it/PrismaSspTest.java b/src/test/java/org/prebid/server/it/PrismaSspTest.java new file mode 100644 index 00000000000..1fedb5a790c --- /dev/null +++ b/src/test/java/org/prebid/server/it/PrismaSspTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class PrismaSspTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromPrismassp() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/prismassp-exchange")) + .withQueryParam("placement", equalTo("placement")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/prismassp/test-prismassp-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/prismassp/test-prismassp-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/prismassp/test-auction-prismassp-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/prismassp/test-auction-prismassp-response.json", + response, + singletonList("prismassp")); + } +} diff --git a/src/test/java/org/prebid/server/it/RediadsTest.java b/src/test/java/org/prebid/server/it/RediadsTest.java new file mode 100644 index 00000000000..6ff3335973c --- /dev/null +++ b/src/test/java/org/prebid/server/it/RediadsTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class RediadsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromRediads() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rediads-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/rediads/test-rediads-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/rediads/test-rediads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/rediads/test-auction-rediads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/rediads/test-auction-rediads-response.json", response, singletonList("rediads")); + } +} diff --git a/src/test/java/org/prebid/server/it/ZentotemTest.java b/src/test/java/org/prebid/server/it/ZentotemTest.java new file mode 100644 index 00000000000..6012e7cfe4a --- /dev/null +++ b/src/test/java/org/prebid/server/it/ZentotemTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class ZentotemTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromZentotem() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/zentotem-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/zentotem/test-zentotem-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/zentotem/test-zentotem-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/zentotem/test-auction-zentotem-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/zentotem/test-auction-zentotem-response.json", response, singletonList("zentotem")); + } +} diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 243ef33e66d..73e9b902168 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -332,6 +332,15 @@ public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics() assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(4); } + @Test + public void updateImpsDroppedMetricShouldIncrementMetrics() { + // when + metrics.updateImpsDroppedMetric(2); + + // then + assertThat(metricRegistry.counter("imps_dropped").getCount()).isEqualTo(2); + } + @Test public void updateDebugRequestsMetricsShouldIncrementMetrics() { // when diff --git a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java index 78ac075c285..7337d5d8b1c 100644 --- a/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java +++ b/src/test/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreatorTest.java @@ -117,7 +117,7 @@ public void createShouldReturnUsersyncerWithPrimaryAndSecondaryMethods() { .build(); assertThat(result).isEqualTo( - Usersyncer.of("rubicon", expectedIframeMethod, expectedRedirectMethod)); + Usersyncer.of("rubicon", expectedIframeMethod, expectedRedirectMethod, false, null)); } @Test diff --git a/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-akcelo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-akcelo-bid-request.json new file mode 100644 index 00000000000..fcae2928304 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-akcelo-bid-request.json @@ -0,0 +1,68 @@ +{ + "id": "test-auction-request", + "imp": [ + { + "id": "imp1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "secure": 1, + "ext": { + "akcelo": { + "adUnitId": 1, + "siteId": 2 + } + } + } + ], + "site": { + "domain": "testpage.com", + "page": "http://testpage.com", + "publisher": { + "domain": "testpage.com", + "ext": { + "prebid": { + "parentAccount": "2" + } + } + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "Mozilla/5.0", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext" : { + "prebid" : { + "channel" : { + "name" : "web" + }, + "server" : { + "externalurl" : "http://localhost:8080", + "gvlid" : 1, + "datacenter" : "local", + "endpoint" : "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-akcelo-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-akcelo-bid-response.json new file mode 100644 index 00000000000..0d282ff107d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-akcelo-bid-response.json @@ -0,0 +1,40 @@ +{ + "id": "test-auction-request", + "seatbid": [ + { + "seat": "adverxo", + "group": 0, + "bid": [ + { + "id": "bid1", + "impid": "imp1", + "price": 1.23, + "adm": "", + "nurl": "https://example.com/win?price=1.23", + "crid": "creative1", + "w": 300, + "h": 250, + "exp": 300, + "mtype": 1, + "ext": { + "origbidcpm": 1.23, + "origbidcur": "USD", + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adverxo": 0 + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-auction-akcelo-request.json b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-auction-akcelo-request.json new file mode 100644 index 00000000000..c4986c14ef9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-auction-akcelo-request.json @@ -0,0 +1,34 @@ +{ + "id": "test-auction-request", + "imp": [ + { + "id": "imp1", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "akcelo": { + "adUnitId": 1, + "siteId": 2 + } + } + } + ], + "site": { + "page": "http://testpage.com" + }, + "device": { + "ua": "Mozilla/5.0" + }, + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-auction-akcelo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-auction-akcelo-response.json new file mode 100644 index 00000000000..5268897f633 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/akcelo/test-auction-akcelo-response.json @@ -0,0 +1,43 @@ +{ + "id": "test-auction-request", + "seatbid": [ + { + "seat": "akcelo", + "group": 0, + "bid": [ + { + "id": "bid1", + "impid": "imp1", + "price": 1.23, + "adm": "", + "nurl": "https://example.com/win?price=1.23", + "crid": "creative1", + "w": 300, + "h": 250, + "exp": 300, + "mtype": 1, + "ext": { + "origbidcpm": 1.23, + "origbidcur": "USD", + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "akcelo" + } + } + } + } + ] + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "akcelo": 0 + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-request.json new file mode 100644 index 00000000000..e1be3a1f864 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "easybid": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-response.json new file mode 100644 index 00000000000..41a9fc23d1c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-auction-easybid-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "easybid" + } + }, + "bidType": "banner" + } + } + ], + "seat": "easybid", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "easybid": "{{ easybid.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/easybid/test-easybid-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-auction-mediasquare-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-auction-mediasquare-request.json new file mode 100644 index 00000000000..100db7e81d2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-auction-mediasquare-request.json @@ -0,0 +1,91 @@ +{ + "id": "70e5672c-515b-406e-967c-fcc2b04de04f", + "imp": [ + { + "id": "2c35e25e-e7d3-41bf-b810-06a449f456b9", + "bidfloor": 1, + "bidfloorcur": "USD", + "banner": { + "w": 970, + "h": 250 + }, + "ext": { + "mediasquare": { + "owner": "test", + "code": "publishername_atf_desktop_rg_pave" + } + } + }, + { + "id": "2059a3e6-71a3-43ea-8290-b5ceb13d35a8", + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + }, + { + "w": 120, + "h": 600 + } + ] + }, + "ext": { + "mediasquare": { + "owner": "test", + "code": "publishername_atf_desktop_rg_pave" + } + } + } + ], + "app": { + "content": { + }, + "domain": "debug.mediasquare.fr", + "id": "app-id-test", + "name": "debug.mediasquare.fr", + "publisher": { + "id": "MEDIA_SQUARE" + } + }, + "device": { + "devicetype": 1, + "geo": { + "country": "FRA", + "ipservice": 3 + }, + "ip": "92.154.6.0", + "language": "fr" + }, + "regs": { + "gdpr": 0, + "ext": { + "dsa": { + "dsarequired": 1, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-auction-mediasquare-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-auction-mediasquare-response.json new file mode 100644 index 00000000000..dcaacc45261 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-auction-mediasquare-response.json @@ -0,0 +1,87 @@ +{ + "id": "70e5672c-515b-406e-967c-fcc2b04de04f", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "2c35e25e-e7d3-41bf-b810-06a449f456b9", + "price": 2, + "adm": "\u003c!-- This is an example --\u003e", + "adomain": [ + "mediasquare.fr" + ], + "crid": "msq_test|fakeCreative", + "w": 250, + "mtype": 1, + "exp": 300, + "ext": { + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": { + "domain": "dsp1domain.com", + "dsaparams": [ + 1, + 2 + ] + }, + "adrender": 1 + }, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "mediasquare", + "advertiserDomains": [ + "mediasquare.fr" + ], + "mediaType": "banner" + } + }, + "origbidcpm": 2, + "origbidcur": "USD" + } + }, + { + "id": "2", + "impid": "2059a3e6-71a3-43ea-8290-b5ceb13d35a8", + "price": 0.02, + "adm": "\u003c!-- This is an example --\u003e", + "adomain": [ + "mediasquare.fr" + ], + "crid": "msq_test|fakeCreative", + "w": 250, + "mtype": 1, + "exp": 300, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "mediasquare", + "advertiserDomains": [ + "mediasquare.fr" + ], + "mediaType": "banner" + } + }, + "origbidcpm": 0.02, + "origbidcur": "USD" + } + } + ], + "seat": "mediasquare", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "mediasquare": "{{ mediasquare.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-mediasquare-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-mediasquare-bid-request.json new file mode 100644 index 00000000000..cf793100948 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-mediasquare-bid-request.json @@ -0,0 +1,110 @@ +{ + "codes": [ + { + "auctionid": "70e5672c-515b-406e-967c-fcc2b04de04f", + "bidid": "2c35e25e-e7d3-41bf-b810-06a449f456b9", + "code": "publishername_atf_desktop_rg_pave", + "owner": "test", + "mediatypes": { + "banner": { + "sizes": [ + [ + 970, + 250 + ] + ] + } + }, + "floor": { + "970x250": { + "floor": 1, + "currency": "USD" + } + } + }, + { + "auctionid": "70e5672c-515b-406e-967c-fcc2b04de04f", + "bidid": "2059a3e6-71a3-43ea-8290-b5ceb13d35a8", + "code": "publishername_atf_desktop_rg_pave", + "owner": "test", + "mediatypes": { + "banner": { + "sizes": [ + [ + 300, + 250 + ], + [ + 300, + 600 + ], + [ + 120, + 600 + ] + ] + } + }, + "floor": { + "120x600": { + "floor": 0.01, + "currency": "USD" + }, + "300x250": { + "floor": 0.01, + "currency": "USD" + }, + "300x600": { + "floor": 0.01, + "currency": "USD" + } + } + } + ], + "gdpr": { + "consent_required": false + }, + "dsa": { + "dsarequired": 1, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + }, + "tech": { + "device": { + "geo": { + "ipservice": 3, + "country": "FRA" + }, + "ip": "92.154.6.0", + "devicetype": 1, + "language": "fr", + "ua": "userAgent" + }, + "app": { + "id": "app-id-test", + "name": "debug.mediasquare.fr", + "domain": "debug.mediasquare.fr", + "publisher": { + "id": "MEDIA_SQUARE" + }, + "content": {} + } + }, + "type": "pbs", + "test": false +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-mediasquare-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-mediasquare-bid-response.json new file mode 100644 index 00000000000..c7140f29661 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mediasquare/test-mediasquare-bid-response.json @@ -0,0 +1,54 @@ +{ + "responses": [ + { + "id": "1", + "ad": "\u003c!-- This is an example --\u003e", + "bid_id": "2c35e25e-e7d3-41bf-b810-06a449f456b9", + "bidder": "msq_test", + "code": "test/publishername_atf_desktop_rg_pave", + "cpm": 2, + "increment": 2, + "currency": "USD", + "creative_id": "msq_test|fakeCreative", + "width": 250, + "net_revenue": true, + "transaction_id": "2c35e25e-e7d3-41bf-b810-06a449f456b9", + "ttl": 20000, + "adomain": [ + "mediasquare.fr" + ], + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": { + "domain": "dsp1domain.com", + "dsaparams": [ + 1, + 2 + ] + }, + "adrender": 1 + }, + "hasConsent": true + }, + { + "id": "2", + "ad": "\u003c!-- This is an example --\u003e", + "bid_id": "2059a3e6-71a3-43ea-8290-b5ceb13d35a8", + "bidder": "msq_test", + "code": "test/publishername_atf_desktop_rg_pave", + "cpm": 0.02, + "increment": 0.02, + "currency": "USD", + "creative_id": "msq_test|fakeCreative", + "width": 250, + "net_revenue": true, + "transaction_id": "2059a3e6-71a3-43ea-8290-b5ceb13d35a8", + "ttl": 20000, + "adomain": [ + "mediasquare.fr" + ], + "hasConsent": true + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json index 5d6eacd1e38..0901179a05d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json @@ -1,15 +1,66 @@ { "adunit" : "imp_id", "currency" : "USD", - "consent_required" : false, "ik" : "request_id", - "referer" : "http://www.example.com", - "referer_canonical" : "www.example.com", "request_id" : "request_id", "timeout" : "${json-unit.any-number}", "params" : { "placement" : "placement", "test" : "test" }, + "ortb2" : { + "id" : "request_id", + "imp" : [ { + "id" : "imp_id", + "banner" : { + "w" : 320, + "h" : 250 + }, + "secure" : 1, + "ext" : { + "tid" : "${json-unit.any-string}", + "bidder" : { + "apiKey" : "apiKey", + "placement" : "placement", + "test" : "test" + } + } + } ], + "site" : { + "domain" : "www.example.com", + "page" : "http://www.example.com", + "publisher" : { + "domain" : "example.com" + }, + "ext" : { + "amp" : 0 + } + }, + "device" : { + "ua" : "userAgent", + "ip" : "193.168.244.1" + }, + "at" : 1, + "tmax" : "${json-unit.any-number}", + "cur" : [ "USD" ], + "source" : { + "tid" : "${json-unit.any-string}" + }, + "regs" : { + "ext" : { + "gdpr" : 0 + } + }, + "ext" : { + "prebid" : { + "server" : { + "externalurl" : "http://localhost:8080", + "gvlid" : 1, + "datacenter" : "local", + "endpoint" : "/openrtb2/auction" + } + } + } + }, "version" : "${json-unit.any-string}" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-request.json new file mode 100644 index 00000000000..015bb3967c8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-response.json new file mode 100644 index 00000000000..4c22dfd3154 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-auction-nexx360-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "nexx360" + } + }, + "bidType": "banner" + } + } + ], + "seat": "nexx360", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "nexx360": "{{ nexx360.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/nexx360/test-nexx360-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-1accord-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-request.json new file mode 100644 index 00000000000..87a3265574b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "1accord": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-response.json new file mode 100644 index 00000000000..e4ea52c0c85 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oneaccord/test-auction-1accord-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "1accord" + } + }, + "bidType": "banner" + } + } + ], + "seat": "1accord", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "1accord": "{{ 1accord.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-auction-optidigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-auction-optidigital-request.json new file mode 100644 index 00000000000..f85e77eb3bf --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-auction-optidigital-request.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "optidigital": { + "publisherId": "testPublisherId", + "placementId": "testPlacementId" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-auction-optidigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-auction-optidigital-response.json new file mode 100644 index 00000000000..91c2bda3b0d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-auction-optidigital-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "optidigital" + } + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "optidigital", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "optidigital": 0 + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-optidigital-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-optidigital-bid-request.json new file mode 100644 index 00000000000..18dee6e53fb --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-optidigital-bid-request.json @@ -0,0 +1,55 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "publisherId": "testPublisherId", + "placementId": "testPlacementId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "gdpr": 0 + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-optidigital-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-optidigital-bid-response.json new file mode 100644 index 00000000000..a4c0edc3e09 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/optidigital/test-optidigital-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-request.json new file mode 100644 index 00000000000..647a01b2a29 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prismassp": { + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-response.json new file mode 100644 index 00000000000..83245ca2001 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-auction-prismassp-response.json @@ -0,0 +1,42 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 5.78, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "prismassp" + } + }, + "bidType": "banner" + } + } + ], + "seat": "prismassp", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "prismassp": "{{ prismassp.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-request.json new file mode 100644 index 00000000000..a6a87d4012c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "nexx360": { + "placement": "placement" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "nexx360": { + "caller": [ + { + "name": "Prebid-Server", + "version": "${json-unit.any-string}" + } + ] + } + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-response.json new file mode 100644 index 00000000000..1e43f621496 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/prismassp/test-prismassp-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 5.78, + "adm": "adm00", + "crid": "crid00", + "w": 300, + "h": 250, + "ext": { + "bidType": "banner" + } + } + ], + "seat": "seatId00", + "group": 0 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json index 964d4ea072b..66a4e966a31 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json @@ -12,12 +12,13 @@ "crid": "crid9", "w": 300, "h": 600, + "mtype": 2, "ext": { - "bidtype": 1, "prebid": { "type": "video", "meta": { - "adaptercode": "pubmatic" + "adaptercode": "pubmatic", + "mediaType": "video" } }, "origbidcpm": 4.75 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-response.json index 713640bcf40..b8a8151aff8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-response.json @@ -9,11 +9,9 @@ "price": 4.75, "adm": "adm9", "crid": "crid9", + "mtype": "2", "w": 300, - "h": 600, - "ext": { - "bidtype": 1 - } + "h": 600 } ], "seat": "seatId9", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-auction-rediads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-auction-rediads-request.json new file mode 100644 index 00000000000..8692b2e22b0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-auction-rediads-request.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "rediads": { + "account_id": "publisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-auction-rediads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-auction-rediads-response.json new file mode 100644 index 00000000000..121de1185c8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-auction-rediads-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "crid": "creativeId", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "rediads" + } + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "rediads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "rediads": "{{ rediads.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-rediads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-rediads-bid-request.json new file mode 100644 index 00000000000..743d5a7ca27 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-rediads-bid-request.json @@ -0,0 +1,52 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}" + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "publisherId", + "domain" : "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "gdpr": 0 + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-rediads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-rediads-bid-response.json new file mode 100644 index 00000000000..180173549d8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rediads/test-rediads-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json b/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json index aab841a83de..7df441df2c4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json @@ -12,6 +12,7 @@ "crid":"banner_creative_id", "w": 300, "h": 250, + "mtype": 1, "ext": { "origbidcpm": 6.77, "origbidcur": "EUR", @@ -30,6 +31,7 @@ "price": 6.494, "adm": "
video
", "crid":"video_creative_id", + "mtype": 2, "ext": { "origbidcpm": 5.7, "origbidcur": "EUR", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-stroeercore-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-stroeercore-bid-response.json index 074d4c2cbf1..fa6d5e0f7ca 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-stroeercore-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-stroeercore-bid-response.json @@ -7,14 +7,16 @@ "width": 300, "height": 250, "ad": "
foo
", - "crid": "banner_creative_id" + "crid": "banner_creative_id", + "mtype": "banner" }, { "id": "bid_id_2", "bidId": "imp_id_2", "cpm": 5.7, "ad": "
video
", - "crid": "video_creative_id" + "crid": "video_creative_id", + "mtype": "video" } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-auction-zentotem-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-auction-zentotem-request.json new file mode 100644 index 00000000000..ce0353e4eba --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-auction-zentotem-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "zentotem": { + "property": "value" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-auction-zentotem-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-auction-zentotem-response.json new file mode 100644 index 00000000000..6aa0fcfc2c2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-auction-zentotem-response.json @@ -0,0 +1,43 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "adm": "adm001", + "adid": "adid", + "cid": "cid", + "crid": "crid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "zentotem" + } + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "zentotem", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "zentotem": "{{ zentotem.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-zentotem-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-zentotem-bid-request.json new file mode 100644 index 00000000000..1d41be4e061 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-zentotem-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid" : "${json-unit.any-string}", + "bidder": { + "property": "value" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs" : { + "ext" : { + "gdpr" : 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-zentotem-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-zentotem-bid-response.json new file mode 100644 index 00000000000..ae69236c864 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/zentotem/test-zentotem-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "mtype": 1, + "h": 250, + "w": 300 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 747b9a2fc6a..01ff79b8691 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -108,6 +108,8 @@ adapters.aidem.enabled=true adapters.aidem.endpoint=http://localhost:8090/aidem-exchange adapters.aja.enabled=true adapters.aja.endpoint=http://localhost:8090/aja +adapters.akcelo.enabled=true +adapters.akcelo.endpoint=http://localhost:8090/akcelo-exchange adapters.algorix.enabled=true adapters.algorix.endpoint=http://localhost:8090/algorix-exchange adapters.alkimi.enabled=true @@ -361,6 +363,8 @@ adapters.medianet.enabled=true adapters.medianet.endpoint=http://localhost:8090/medianet-exchange adapters.melozen.enabled=true adapters.melozen.endpoint=http://localhost:8090/melozen-exchange?pubId={{PublisherID}} +adapters.mediasquare.enabled=true +adapters.mediasquare.endpoint=http://localhost:8090/mediasquare-exchange adapters.metax.enabled=true adapters.metax.endpoint=http://localhost:8090/metax-exchange?publisher_id={{publisherId}}&adunit={{adUnit}} adapters.mgid.enabled=true @@ -382,6 +386,14 @@ adapters.motorik.endpoint=http://localhost:8090/motorik-exchange?k={{AccountID}} adapters.nextmillennium.enabled=true adapters.nextmillennium.endpoint=http://localhost:8090/nextmillennium-exchange adapters.nextmillennium.extra-info.nmmFlags=1 +adapters.nexx360.enabled=true +adapters.nexx360.endpoint=http://localhost:8090/nexx360-exchange +adapters.nexx360.aliases.1accord.enabled=true +adapters.nexx360.aliases.1accord.endpoint=http://localhost:8090/1accord-exchange +adapters.nexx360.aliases.prismassp.enabled=true +adapters.nexx360.aliases.prismassp.endpoint=http://localhost:8090/prismassp-exchange +adapters.nexx360.aliases.easybid.enabled=true +adapters.nexx360.aliases.easybid.endpoint=http://localhost:8090/easybid-exchange adapters.nobid.enabled=true adapters.nobid.endpoint=http://localhost:8090/nobid-exchange?pubid= adapters.ogury.enabled=true @@ -396,6 +408,8 @@ adapters.openx.enabled=true adapters.openx.endpoint=http://localhost:8090/openx-exchange adapters.operaads.enabled=true adapters.operaads.endpoint=http://localhost:8090/operaads-exchange +adapters.optidigital.enabled=true +adapters.optidigital.endpoint=http://localhost:8090/optidigital-exchange adapters.oraki.enabled=true adapters.oraki.endpoint=http://localhost:8090/oraki-exchange adapters.orbidder.enabled=true @@ -426,6 +440,9 @@ adapters.qt.enabled=true adapters.qt.endpoint=http://localhost:8090/qt-exchange adapters.readpeak.enabled=true adapters.readpeak.endpoint=http://localhost:8090/readpeak-exchange +adapters.rediads.enabled=true +adapters.rediads.endpoint=http://{{SUBDOMAIN}}:8090/rediads-exchange +adapters.rediads.default-subdomain=localhost adapters.relevantdigital.enabled=true adapters.relevantdigital.endpoint=http://localhost:8090/relevantdigital-exchange?pbsHost={{Host}} adapters.resetdigital.enabled=true @@ -603,6 +620,8 @@ adapters.yieldmo.enabled=true adapters.yieldmo.endpoint=http://localhost:8090/yieldmo-exchange adapters.yieldone.enabled=true adapters.yieldone.endpoint=http://localhost:8090/yieldone-exchange +adapters.zentotem.enabled=true +adapters.zentotem.endpoint=http://localhost:8090/zentotem-exchange adapters.zeroclickfraud.enabled=true adapters.zeroclickfraud.endpoint=http://{{Host}}/zeroclickfraud-exchange?sid={{SourceId}} adapters.aax.enabled=true