diff --git a/.github/workflows/scripts/codepath-notification b/.github/workflows/scripts/codepath-notification index 371c86fc652..7884219ef5b 100644 --- a/.github/workflows/scripts/codepath-notification +++ b/.github/workflows/scripts/codepath-notification @@ -24,3 +24,4 @@ medianet|Medianet: prebid@media.net thetradedesk|TheTradeDesk: Prebid-Maintainers@thetradedesk.com gumgum|GumGum: prebid@gumgum.com kargo|Kargo: kraken@kargo.com +bidwave|Bidwave: prebid@bidwave.net diff --git a/src/main/java/org/prebid/server/bidder/adswag/AdswagBidder.java b/src/main/java/org/prebid/server/bidder/adswag/AdswagBidder.java new file mode 100644 index 00000000000..065e946d335 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adswag/AdswagBidder.java @@ -0,0 +1,331 @@ +package org.prebid.server.bidder.adswag; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +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.adswag.ExtImpAdswag; +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.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +public class AdswagBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String DEFAULT_CURRENCY = "EUR"; + private static final Pattern VAST_MARKUP = Pattern.compile("<\\s*VAST[\\s/>]", Pattern.CASE_INSENSITIVE); + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdswagBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final Map> impsByPublisher = new LinkedHashMap<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpAdswag extImpAdswag; + try { + extImpAdswag = parseImpExt(imp); + validateImpExt(extImpAdswag, imp.getId()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + + impsByPublisher + .computeIfAbsent(extImpAdswag.getPublisherId(), _ -> new ArrayList<>()) + .add(modifyImp(imp, extImpAdswag.getPlacementId())); + } + + if (impsByPublisher.isEmpty()) { + return Result.withErrors(errors); + } + + if (request.getSite() == null && request.getApp() == null) { + errors.add(BidderError.badInput("request must contain either site or app")); + return Result.withErrors(errors); + } + + final List> httpRequests = impsByPublisher.entrySet().stream() + .map(group -> modifyBidRequest(request, group.getKey(), group.getValue())) + .map(modifiedRequest -> BidderUtil.defaultRequest(modifiedRequest, endpointUrl, mapper)) + .toList(); + + return Result.of(httpRequests, errors); + } + + private ExtImpAdswag parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext for impression " + imp.getId()); + } + } + + private static void validateImpExt(ExtImpAdswag extImpAdswag, String impId) { + if (StringUtils.isBlank(extImpAdswag.getPublisherId())) { + throw new PreBidException("missing publisherId for imp " + impId); + } + } + + private static Imp modifyImp(Imp imp, String placementId) { + final ObjectNode modifiedExt = imp.getExt().deepCopy(); + modifiedExt.remove("bidder"); + modifiedExt.remove("prebid"); + if (StringUtils.isNotBlank(placementId)) { + modifiedExt.putObject("adswag").put("placement_id", placementId); + } + + return imp.toBuilder().ext(modifiedExt.isEmpty() ? null : modifiedExt).build(); + } + + private static BidRequest modifyBidRequest(BidRequest request, String publisherId, List modifiedImps) { + return request.toBuilder() + .site(modifySite(request.getSite(), publisherId)) + .app(modifyApp(request.getApp(), publisherId)) + .imp(modifiedImps) + .build(); + } + + private static Site modifySite(Site site, String publisherId) { + return site != null + ? site.toBuilder().publisher(modifyPublisher(site.getPublisher(), publisherId)).build() + : null; + } + + private static App modifyApp(App app, String publisherId) { + return app != null + ? app.toBuilder().publisher(modifyPublisher(app.getPublisher(), publisherId)).build() + : null; + } + + private static Publisher modifyPublisher(Publisher publisher, String publisherId) { + return Optional.ofNullable(publisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .id(publisherId) + .build(); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final String body = httpCall.getResponse().getBody(); + if (StringUtils.isBlank(body)) { + return Result.empty(); + } + + try { + final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); + final List errors = new ArrayList<>(); + final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); + return Result.of(bids, errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidRequest bidRequest, BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final String currency = StringUtils.defaultIfBlank(bidResponse.getCur(), DEFAULT_CURRENCY); + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(List::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidRequest.getImp(), currency, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { + final Imp imp = findImp(imps, bid.getImpid()); + if (imp == null) { + errors.add(BidderError.badServerResponse( + "bid %s references unknown imp %s".formatted(bid.getId(), bid.getImpid()))); + return null; + } + + final String serveUrl = serveUrl(bid); + if (StringUtils.isBlank(bid.getAdm()) && StringUtils.isBlank(serveUrl)) { + errors.add(BidderError.badServerResponse( + "bid %s has neither adm nor ext.adswag.serve_url".formatted(bid.getId()))); + return null; + } + + final BidType bidType; + try { + bidType = resolveBidType(imp, bid); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + final Bid modifiedBid = switch (bidType) { + case banner -> makeBannerBid(bid, serveUrl, imp.getBanner()); + case video -> makeVideoBid(bid, serveUrl, imp.getVideo()); + case audio -> makeAudioBid(bid, serveUrl); + // should never happen + default -> throw new AssertionError(); + }; + + return BidderBid.of(modifiedBid, bidType, currency); + } + + private static Imp findImp(List imps, String impId) { + return imps.stream() + .filter(imp -> Objects.equals(imp.getId(), impId)) + .findFirst() + .orElse(null); + } + + private static String serveUrl(Bid bid) { + final JsonNode serveUrlNode = bid.getExt() != null + ? bid.getExt().path("adswag").path("serve_url") + : null; + return serveUrlNode != null && serveUrlNode.isTextual() ? serveUrlNode.textValue() : null; + } + + private static BidType resolveBidType(Imp imp, Bid bid) { + final Integer mtype = bid.getMtype(); + if (mtype != null) { + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + default -> throw new PreBidException( + "unsupported bid.mtype %d for impression %s".formatted(mtype, bid.getImpid())); + }; + } + + final boolean isVast = bid.getAdm() != null && VAST_MARKUP.matcher(bid.getAdm()).find(); + if (imp.getAudio() != null && (isVast || imp.getBanner() == null)) { + return BidType.audio; + } + if (imp.getVideo() != null && (isVast || imp.getBanner() == null)) { + return BidType.video; + } + if (imp.getBanner() != null) { + return BidType.banner; + } + throw new PreBidException("unable to resolve media type for impression " + bid.getImpid()); + } + + private static Bid makeBannerBid(Bid bid, String serveUrl, Banner banner) { + final Format size = bannerSize(banner); + final boolean updateSize = hasNoSize(bid) && size != null; + + return bid.toBuilder() + .adm(StringUtils.isBlank(bid.getAdm()) ? iframeMarkup(serveUrl, size) : bid.getAdm()) + .w(updateSize ? size.getW() : bid.getW()) + .h(updateSize ? size.getH() : bid.getH()) + .mtype(1) + .build(); + } + + private static Format bannerSize(Banner banner) { + if (banner == null) { + return null; + } + if (CollectionUtils.isNotEmpty(banner.getFormat())) { + return banner.getFormat().getFirst(); + } + if (banner.getW() != null && banner.getH() != null) { + return Format.builder().w(banner.getW()).h(banner.getH()).build(); + } + return null; + } + + private static String iframeMarkup(String serveUrl, Format size) { + final StringBuilder markup = new StringBuilder() + .append(""""); + + return markup.toString(); + } + + // Mirrors Go's html.EscapeString so both server adapters emit identical markup. + private static String escapeHtmlAttribute(String value) { + return value + .replace("&", "&") + .replace("'", "'") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private static boolean hasNoSize(Bid bid) { + return bid.getW() == null || bid.getW() <= 0 || bid.getH() == null || bid.getH() <= 0; + } + + private static Bid makeVideoBid(Bid bid, String serveUrl, Video video) { + final boolean updateSize = hasNoSize(bid) && video != null && video.getW() != null && video.getH() != null; + return bid.toBuilder() + .nurl(StringUtils.isBlank(bid.getAdm()) ? serveUrl : bid.getNurl()) + .w(updateSize ? video.getW() : bid.getW()) + .h(updateSize ? video.getH() : bid.getH()) + .mtype(2) + .build(); + } + + private static Bid makeAudioBid(Bid bid, String serveUrl) { + return bid.toBuilder() + .nurl(StringUtils.isBlank(bid.getAdm()) ? serveUrl : bid.getNurl()) + .mtype(3) + .build(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/bidwave/BidwaveBidder.java b/src/main/java/org/prebid/server/bidder/bidwave/BidwaveBidder.java new file mode 100644 index 00000000000..ab602b009d3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidwave/BidwaveBidder.java @@ -0,0 +1,201 @@ +package org.prebid.server.bidder.bidwave; + +import com.fasterxml.jackson.core.type.TypeReference; +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.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.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +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.bidwave.ExtImpBidwave; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class BidwaveBidder implements Bidder { + + private static final String BIDWAVE_EXT = "bidwave"; + private static final String PUBLISHER_ID_EXT = "pid"; + private static final String BIDDER_CURRENCY = "USD"; + private static final List DEFAULT_CURRENCY = Collections.singletonList(BIDDER_CURRENCY); + private static final TypeReference> BIDWAVE_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + + public BidwaveBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final Map> impsByPublisherId = new LinkedHashMap<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final String publisherId = parsePublisherId(imp); + final Imp modifiedImp = modifyImpCurrency(imp, bidRequest); + impsByPublisherId.computeIfAbsent(publisherId, ignored -> new ArrayList<>()).add(modifiedImp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + final List> requests = impsByPublisherId.entrySet().stream() + .map(entry -> BidderUtil.defaultRequest( + createRequest(bidRequest, entry.getValue(), entry.getKey()), endpointUrl, mapper)) + .toList(); + + return Result.of(requests, errors); + } + + private String parsePublisherId(Imp imp) { + final ExtImpBidwave extImpBidwave; + try { + extImpBidwave = mapper.mapper().convertValue(imp.getExt(), BIDWAVE_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid imp.ext for impression %s. Error Information: %s" + .formatted(imp.getId(), e.getMessage())); + } + + return extImpBidwave.getPublisherId(); + } + + private Imp modifyImpCurrency(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + if (!BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY)) { + return imp; + } + + try { + final BigDecimal convertedBidFloor = currencyConversionService.convertCurrency( + initialBidFloorPrice.getValue(), + bidRequest, + initialBidFloorPrice.getCurrency(), + BIDDER_CURRENCY); + return imp.toBuilder() + .bidfloor(convertedBidFloor) + .bidfloorcur(BIDDER_CURRENCY) + .build(); + } catch (PreBidException e) { + throw new PreBidException( + "expected currency USD for bid floor; unable to convert from %s for imp `%s`" + .formatted(initialBidFloorPrice.getCurrency(), imp.getId())); + } + } + + private BidRequest createRequest(BidRequest bidRequest, List imps, String publisherId) { + return bidRequest.toBuilder() + .imp(imps) + .cur(DEFAULT_CURRENCY) + .ext(createRequestExt(bidRequest.getExt(), publisherId)) + .build(); + } + + private ExtRequest createRequestExt(ExtRequest requestExt, String publisherId) { + final ExtRequest updatedExt = ExtRequest.of(requestExt != null ? requestExt.getPrebid() : null); + if (requestExt != null) { + mapper.fillExtension(updatedExt, requestExt); + } + + final ObjectNode bidwaveExt = mapper.mapper().createObjectNode().put(PUBLISHER_ID_EXT, publisherId); + updatedExt.addProperty(BIDWAVE_EXT, bidwaveExt); + return updatedExt; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + final List bids = extractBids(bidResponse, errors); + return Result.of(bids, errors); + } + + private static List extractBids(BidResponse bidResponse, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final String currency = bidResponse.getCur(); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, currency, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, + String currency, + List errors) { + + final BidType bidType; + try { + bidType = resolveBidType(bid); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, currency); + } + + private static BidType resolveBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Bid must have non-zero MType for impression with ID: \"%s\"" + .formatted(bid.getImpid())); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + default -> throw new PreBidException("Unsupported MType %d for impression with ID: \"%s\"" + .formatted(markupType, bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java index 4c64992184b..163963a325e 100644 --- a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java +++ b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java @@ -1,12 +1,15 @@ package org.prebid.server.bidder.connectad; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; 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 io.vertx.core.MultiMap; @@ -31,13 +34,19 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; public class ConnectAdBidder implements Bidder { private static final TypeReference> CONNECTAD_EXT_TYPE_REFERENCE = new TypeReference<>() { }; + private static final String BIDDER_EXT_KEY = "bidder"; + private static final String NETWORK_ID_FIELD = "networkId"; + private static final String SITE_ID_FIELD = "siteId"; private static final String HTTPS_PREFIX = "https"; private final String endpointUrl; @@ -58,7 +67,7 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { final ExtImpConnectAd impExt = parseImpExt(imp); - final Imp updatedImp = updateImp(imp, secure, impExt.getSiteId(), impExt.getBidFloor()); + final Imp updatedImp = updateImp(imp, secure, impExt); processedImps.add(updatedImp); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -91,27 +100,40 @@ private ExtImpConnectAd parseImpExt(Imp imp) { } catch (IllegalArgumentException e) { throw new PreBidException("Impression id=%s, has invalid Ext".formatted(imp.getId())); } - final String siteId = extImpConnectAd.getSiteId(); - if (siteId == null) { + if (extImpConnectAd.getSiteId() == null) { throw new PreBidException("Impression id=%s, has no siteId present".formatted(imp.getId())); } return extImpConnectAd; } - private Imp updateImp(Imp imp, Integer secure, String siteId, BigDecimal bidFloor) { + private static Imp updateImp(Imp imp, int secure, ExtImpConnectAd extImpConnectAd) { + final BigDecimal bidFloor = extImpConnectAd.getBidFloor(); final boolean isValidBidFloor = BidderUtil.isValidPrice(bidFloor); return imp.toBuilder() .banner(updateBanner(imp.getBanner())) - .tagid(siteId) + .tagid(String.valueOf(extImpConnectAd.getSiteId())) .secure(secure) .bidfloor(isValidBidFloor ? bidFloor : imp.getBidfloor()) .bidfloorcur(isValidBidFloor ? "USD" : imp.getBidfloorcur()) + .ext(modifyImpExt(imp.getExt())) .build(); } + private static ObjectNode modifyImpExt(ObjectNode impExt) { + final ObjectNode modifiedExt = impExt.deepCopy(); + final JsonNode bidder = modifiedExt.get(BIDDER_EXT_KEY); + if (bidder.has(NETWORK_ID_FIELD)) { + modifiedExt.set(NETWORK_ID_FIELD, bidder.get(NETWORK_ID_FIELD)); + } + if (bidder.has(SITE_ID_FIELD)) { + modifiedExt.set(SITE_ID_FIELD, bidder.get(SITE_ID_FIELD)); + } + return modifiedExt; + } + private static Banner updateBanner(Banner banner) { if (banner == null) { - throw new PreBidException("We need a Banner Object in the request"); + return null; } if (banner.getW() != null || banner.getH() != null) { @@ -153,24 +175,42 @@ private static MultiMap resolveHeaders(Device device) { public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse)); + return Result.withValues(extractBids(bidResponse, bidRequest)); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse, BidRequest bidRequest) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } + final Map impIdToImp = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid, impIdToImp), bidResponse.getCur())) .toList(); } + + private static BidType getBidType(Bid bid, Map impIdToImp) { + final Integer mType = bid.getMtype(); + if (mType == null) { + return BidderUtil.getBidType(bid, impIdToImp); + } + + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> BidType.banner; + }; + } } diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java new file mode 100644 index 00000000000..7e51b073b12 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -0,0 +1,207 @@ +package org.prebid.server.bidder.floxis; + +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.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.floxis.ExtImpFloxis; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.Uri; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +public class FloxisBidder implements Bidder { + + private static final TypeReference> FLOXIS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String HOST_MACRO = "Host"; + private static final String SEAT_MACRO = "SeatId"; + + private static final String DEFAULT_REGION = "us-e"; + private static final String DEFAULT_PARTNER = "floxis"; + + private final Uri endpointUrl; + private final JacksonMapper mapper; + + public FloxisBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = Uri.of(endpointUrl); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final Map> impsByHost = new LinkedHashMap<>(); + + for (Imp imp : request.getImp()) { + final ExtImpFloxis impExt; + try { + impExt = parseImpExt(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + + impsByHost.computeIfAbsent( + new HostId(impExt.getSeat(), resolveBidHost(impExt)), key -> new ArrayList<>()) + .add(imp); + } + + final List> httpRequests = impsByHost.entrySet().stream() + .map(entry -> BidderUtil.defaultRequest( + request.toBuilder().imp(entry.getValue()).build(), + resolveUrl(entry.getKey()), + mapper)) + .toList(); + + return Result.of(httpRequests, errors); + } + + private ExtImpFloxis parseImpExt(Imp imp) { + final ExtImpFloxis impExt; + try { + impExt = mapper.mapper().convertValue(imp.getExt(), FLOXIS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid imp.ext.bidder for imp %s: %s".formatted(imp.getId(), e.getMessage())); + } + + final String region = StringUtils.isBlank(impExt.getRegion()) + ? DEFAULT_REGION + : impExt.getRegion().toLowerCase(Locale.ROOT); + final String partner = StringUtils.isBlank(impExt.getPartner()) + ? DEFAULT_PARTNER + : impExt.getPartner().toLowerCase(Locale.ROOT); + + return ExtImpFloxis.of(impExt.getSeat(), region, partner); + } + + private String resolveUrl(HostId hostId) { + return endpointUrl + .replaceMacro(HOST_MACRO, hostId.host()) + .replaceMacro(SEAT_MACRO, hostId.seat()) + .expand(); + } + + private static String resolveBidHost(ExtImpFloxis impExt) { + final String partner = impExt.getPartner(); + final String region = impExt.getRegion(); + return partner.equals(DEFAULT_PARTNER) ? region : partner + "-" + region; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + final List bidderBids = extractBids(bidResponse, bidRequest.getImp(), errors); + return Result.of(bidderBids, errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List imps, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, imps, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { + try { + return BidderBid.of(bid, getMediaTypeForBid(imps, bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getMediaTypeForBid(List imps, Bid bid) { + final Integer mtype = bid.getMtype(); + if (mtype != null && mtype != 0) { + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "unsupported bid.mtype %d for impression %s".formatted(mtype, bid.getImpid())); + }; + } + + final Imp imp = imps.stream() + .filter(currentImp -> Objects.equals(currentImp.getId(), bid.getImpid())) + .findFirst() + .orElseThrow(() -> new PreBidException( + "unable to find impression %s for bid".formatted(bid.getImpid()))); + + if (countFormats(imp) != 1) { + throw new PreBidException( + "unable to resolve a single media type for impression %s; set bid.mtype" + .formatted(bid.getImpid())); + } + + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getAudio() != null) { + return BidType.audio; + } else { + return BidType.xNative; + } + } + + private static int countFormats(Imp imp) { + int formats = 0; + if (imp.getBanner() != null) { + formats++; + } + if (imp.getVideo() != null) { + formats++; + } + if (imp.getAudio() != null) { + formats++; + } + if (imp.getXNative() != null) { + formats++; + } + return formats; + } + + private record HostId(String seat, String host) { + } +} 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 2f9722b2c82..f01c6a81e79 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -2,10 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; import lombok.Builder; import lombok.Value; import java.math.BigDecimal; +import java.util.List; @Value @Builder(toBuilder = true) @@ -16,6 +18,11 @@ public class MissenaAdRequest { String currency; + Boolean debug; + + @JsonProperty("userEids") + List userEids; + BigDecimal floor; String floorCurrency; diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java index 6a34c31efbf..acec2fa0199 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java @@ -16,6 +16,10 @@ public class MissenaAdResponse { String currency; + Integer width; + + Integer height; + @JsonProperty("requestId") String requestId; } 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 4cfaa156e9b..1194d57444e 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -3,11 +3,14 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -21,6 +24,7 @@ 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.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; @@ -42,6 +46,9 @@ public class MissenaBidder implements Bidder { private static final String USD_CURRENCY = "USD"; private static final String EUR_CURRENCY = "EUR"; private static final String PUBLISHER_ID_MACRO = "PublisherID"; + private static final String VERSION_NAME = "prebid-server-java"; + private static final String VERSION_UNKNOWN = "unknown"; + private static final String VERSION_SEPARATOR = "@"; private final Uri endpointUrl; private final JacksonMapper mapper; @@ -91,22 +98,25 @@ private HttpRequest makeHttpRequest(BidRequest request, Imp im final Price floorInfo = resolveBidFloor(imp, request, requestCurrency); final MissenaUserParams userParams = MissenaUserParams.builder() + .apiKey(extImp.getApiKey()) .formats(extImp.getFormats()) .placement(extImp.getPlacement()) - .testMode(extImp.getTestMode()) + .sample(extImp.getSample()) .settings(extImp.getSettings()) .build(); final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() .adUnit(imp.getId()) .currency(requestCurrency) + .debug(Objects.equals(request.getTest(), 1) ? Boolean.TRUE : null) + .userEids(resolveUserEids(request.getUser())) .floor(floorInfo.getValue()) .floorCurrency(floorInfo.getCurrency()) .idempotencyKey(request.getId()) .requestId(request.getId()) .timeout(request.getTmax()) .params(userParams) - .version(prebidVersionProvider.getNameVersionRecord()) + .version(resolveVersion()) .bidRequest(request) .build(); @@ -120,6 +130,17 @@ private HttpRequest makeHttpRequest(BidRequest request, Imp im .build(); } + private String resolveVersion() { + final String version = StringUtils.substringAfter(prebidVersionProvider.getNameVersionRecord(), "/"); + return VERSION_NAME + VERSION_SEPARATOR + (StringUtils.isNotEmpty(version) ? version : VERSION_UNKNOWN); + } + + private static List resolveUserEids(User user) { + final ExtUser extUser = user != null ? user.getExt() : null; + final List eids = extUser != null ? extUser.getEids() : null; + return CollectionUtils.isNotEmpty(eids) ? eids : null; + } + private Price resolveBidFloor(Imp imp, BidRequest bidRequest, String targetCurrency) { final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); return BidderUtil.isValidPrice(initialBidFloorPrice) @@ -202,6 +223,8 @@ private BidderBid extractBid(BidRequest request, MissenaAdResponse response) { .impid(request.getImp().getFirst().getId()) .adm(response.getAd()) .crid(response.getRequestId()) + .w(response.getWidth()) + .h(response.getHeight()) .build(); return BidderBid.of(bid, BidType.banner, response.getCurrency()); diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java b/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java index e63a704a60c..bc97b9242bc 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java @@ -1,7 +1,7 @@ package org.prebid.server.bidder.missena; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; // Changed import +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; @@ -11,13 +11,14 @@ @Value public class MissenaUserParams { + @JsonProperty("apiKey") + String apiKey; + List formats; String placement; - @JsonProperty("test") - String testMode; + String sample; ObjectNode settings; } - diff --git a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java index 8d45320cb9c..19717bbc951 100644 --- a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java +++ b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Publisher; @@ -88,6 +89,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final BidRequest outgoingRequest = bidRequest.toBuilder() .cur(Collections.singletonList(BIDDER_CURRENCY)) .site(modifySite(bidRequest.getSite(), publisherId)) + .app(modifyApp(bidRequest.getApp(), publisherId)) .imp(modifiedImps) .build(); @@ -178,24 +180,33 @@ private static Optional extractPbadslot(Imp imp) { } private Site modifySite(Site site, String publisherId) { + return Optional.ofNullable(site) + .map(Site::toBuilder) + .map(builder -> builder.publisher(modifyPublisher(site.getPublisher(), publisherId))) + .map(Site.SiteBuilder::build) + .orElse(site); + } + + private App modifyApp(App app, String publisherId) { + return Optional.ofNullable(app) + .map(App::toBuilder) + .map(builder -> builder.publisher(modifyPublisher(app.getPublisher(), publisherId))) + .map(App.AppBuilder::build) + .orElse(app); + } + + private Publisher modifyPublisher(Publisher publisher, String publisherId) { final ObjectNode prebidNode = mapper.mapper().createObjectNode(); prebidNode.put("publisherId", publisherId); final ExtPublisher extPublisher = ExtPublisher.empty(); extPublisher.addProperty("prebid", prebidNode); - final Publisher publisher = Optional.ofNullable(site) - .map(Site::getPublisher) + return Optional.ofNullable(publisher) .map(Publisher::toBuilder) .orElseGet(Publisher::builder) .ext(extPublisher) .build(); - - return Optional.ofNullable(site) - .map(Site::toBuilder) - .orElseGet(Site::builder) - .publisher(publisher) - .build(); } @Override diff --git a/src/main/java/org/prebid/server/bidder/sevio/SevioBidder.java b/src/main/java/org/prebid/server/bidder/sevio/SevioBidder.java new file mode 100644 index 00000000000..434ba516aeb --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/sevio/SevioBidder.java @@ -0,0 +1,96 @@ +package org.prebid.server.bidder.sevio; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +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; + +public class SevioBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SevioBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.6"); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, headers, endpointUrl, mapper)); + } + + @Override + public final 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 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 -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static 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 static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + if (mtype == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (mtype) { + case 1 -> BidType.banner; + case 4 -> BidType.xNative; + default -> throw new PreBidException( + "failed to parse bid mtype (%d) for impression id \"%s\"".formatted(mtype, bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java index ce7e28fa8ec..b40a877242a 100644 --- a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -215,6 +215,7 @@ 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( diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adswag/ExtImpAdswag.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adswag/ExtImpAdswag.java new file mode 100644 index 00000000000..2de23e9340f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adswag/ExtImpAdswag.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.adswag; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdswag { + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidwave/ExtImpBidwave.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidwave/ExtImpBidwave.java new file mode 100644 index 00000000000..83d38e74702 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidwave/ExtImpBidwave.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.bidwave; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBidwave { + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java index a75c4846f64..cb4e0badd21 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java @@ -9,10 +9,10 @@ public class ExtImpConnectAd { @JsonProperty("networkId") - String networkId; + Integer networkId; @JsonProperty("siteId") - String siteId; + Integer siteId; @JsonProperty("bidfloor") BigDecimal bidFloor; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java new file mode 100644 index 00000000000..e1302852cde --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/floxis/ExtImpFloxis.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.floxis; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpFloxis { + + String seat; + + String region; + + String partner; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java index 0c3a8fd08ab..bf11d296186 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java @@ -18,8 +18,7 @@ public class ExtImpMissena { String placement; - @JsonProperty("test") - String testMode; + String sample; ObjectNode settings; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdswagConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdswagConfiguration.java new file mode 100644 index 00000000000..4a6e4d5bc53 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdswagConfiguration.java @@ -0,0 +1,35 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adswag.AdswagBidder; +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.env.YamlPropertySourceFactory; +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; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/adswag.yaml", factory = YamlPropertySourceFactory.class) +public class AdswagConfiguration { + + private static final String BIDDER_NAME = "adswag"; + + @Bean("adswagConfigurationProperties") + @ConfigurationProperties("adapters.adswag") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adswagBidderDeps(BidderConfigurationProperties adswagConfigurationProperties, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adswagConfigurationProperties) + .bidderCreator(config -> new AdswagBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidwaveConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidwaveConfiguration.java new file mode 100644 index 00000000000..5a49fc29ea6 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidwaveConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bidwave.BidwaveBidder; +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; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +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; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/bidwave.yaml", factory = YamlPropertySourceFactory.class) +public class BidwaveConfiguration { + + private static final String BIDDER_NAME = "bidwave"; + + @Bean("bidwaveConfigurationProperties") + @ConfigurationProperties("adapters.bidwave") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bidwaveBidderDeps(BidderConfigurationProperties bidwaveConfigurationProperties, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bidwaveConfigurationProperties) + .bidderCreator(config -> new BidwaveBidder(config.getEndpoint(), currencyConversionService, mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java new file mode 100644 index 00000000000..896ac465fbe --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java @@ -0,0 +1,35 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.floxis.FloxisBidder; +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.env.YamlPropertySourceFactory; +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; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/floxis.yaml", factory = YamlPropertySourceFactory.class) +public class FloxisConfiguration { + + private static final String BIDDER_NAME = "floxis"; + + @Bean("floxisConfigurationProperties") + @ConfigurationProperties("adapters.floxis") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps floxisBidderDeps(BidderConfigurationProperties floxisConfigurationProperties, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(floxisConfigurationProperties) + .bidderCreator(config -> new FloxisBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SevioConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SevioConfiguration.java new file mode 100644 index 00000000000..190886a4d3e --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SevioConfiguration.java @@ -0,0 +1,34 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.sevio.SevioBidder; +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.env.YamlPropertySourceFactory; +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; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/sevio.yaml", factory = YamlPropertySourceFactory.class) +public class SevioConfiguration { + + private static final String BIDDER_NAME = "sevio"; + + @Bean("sevioConfigurationProperties") + @ConfigurationProperties("adapters.sevio") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps sevioBidderDeps(BidderConfigurationProperties sevioConfigurationProperties, JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(sevioConfigurationProperties) + .bidderCreator(config -> new SevioBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/validation/ImpValidator.java b/src/main/java/org/prebid/server/validation/ImpValidator.java index c254ac00bfc..7462b1ab3d3 100644 --- a/src/main/java/org/prebid/server/validation/ImpValidator.java +++ b/src/main/java/org/prebid/server/validation/ImpValidator.java @@ -647,8 +647,8 @@ private void validateMetrics(List metrics, String msgPrefix) throws Vali } final Float value = metric.getValue(); - if (value == null || value < 0.0 || value > 1.0) { - throw new ValidationException("%s.metric[%d].value must be in the range [0.0, 1.0]", msgPrefix, i); + if (value == null) { + throw new ValidationException("Missing %s.metric[%d].value", msgPrefix, i); } } } diff --git a/src/main/resources/bidder-config/adagio.yaml b/src/main/resources/bidder-config/adagio.yaml index 9848b3135f2..62915cf545b 100644 --- a/src/main/resources/bidder-config/adagio.yaml +++ b/src/main/resources/bidder-config/adagio.yaml @@ -23,5 +23,5 @@ adapters: usersync: cookie-family-name: adagio iframe: - url: https://u-REGION.4dex.io/pbserver/usync.html?gdpr={gdpr}&gdpr_consent={gdpr_consent}&us_privacy={us_privacy}&gpp={gpp}&&gpp_sid={gpp_sid}&r={redirect_url} + url: https://u-REGION.4dex.io/pbserver/usync.html?gdpr={gdpr}&gdpr_consent={gdpr_consent}&us_privacy={us_privacy}&gpp={gpp}&gpp_sid={gpp_sid}&r={redirect_url} uid-macro: '{UID}' diff --git a/src/main/resources/bidder-config/adswag.yaml b/src/main/resources/bidder-config/adswag.yaml new file mode 100644 index 00000000000..0f40b19e4d2 --- /dev/null +++ b/src/main/resources/bidder-config/adswag.yaml @@ -0,0 +1,26 @@ +adapters: + adswag: + ortb-version: "2.6" + endpoint: https://bid.adswag.ai/prebid/bid + modifying-vast-xml-allowed: false + geoscope: + - EEA + ortb: + multiformat-supported: true + usersync: + cookie-family-name: adswag + redirect: + url: https://ev.adswag.ai/sync/pixel?gdpr={gdpr}&gdpr_consent={gdpr_consent}&us_privacy={us_privacy}&gpp={gpp}&gpp_sid={gpp_sid}&redirect={redirect_url} + uid-macro: '$UID' + meta-info: + maintainer-email: prebid@adswag.ai + app-media-types: + - banner + - video + - audio + site-media-types: + - banner + - video + - audio + supported-vendors: + vendor-id: 1417 diff --git a/src/main/resources/bidder-config/bidwave.yaml b/src/main/resources/bidder-config/bidwave.yaml new file mode 100644 index 00000000000..65bd02fb891 --- /dev/null +++ b/src/main/resources/bidder-config/bidwave.yaml @@ -0,0 +1,14 @@ +adapters: + bidwave: + endpoint: https://rtb.bidwave.net/rtb/v1/bid + ortb-version: "2.6" + meta-info: + maintainer-email: prebid@bidwave.net + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/connectad.yaml b/src/main/resources/bidder-config/connectad.yaml index d6f9db7301a..26d10dc57ed 100644 --- a/src/main/resources/bidder-config/connectad.yaml +++ b/src/main/resources/bidder-config/connectad.yaml @@ -2,18 +2,25 @@ adapters: connectad: # Please uncomment the appropriate endpoint URL for your datacenter # Europe - endpoint: "http://bidder.connectad.io/API?src=pbs" + endpoint: "https://bidder.connectad.io/API?src=pbs" + ortb-version: "2.6" # North/South America - # endpoint: "http://bidder-us.connectad.io/API?src=pbs" + # endpoint: "https://bidder-us.connectad.io/API?src=pbs" # APAC - # endpoint: "http://bidder-apac.connectad.io/API?src=pbs" + # endpoint: "https://bidder-apac.connectad.io/API?src=pbs" endpoint-compression: gzip meta-info: maintainer-email: support@connectad.io app-media-types: - banner + - video + - native + - audio site-media-types: - banner + - video + - native + - audio supported-vendors: vendor-id: 138 usersync: diff --git a/src/main/resources/bidder-config/floxis.yaml b/src/main/resources/bidder-config/floxis.yaml new file mode 100644 index 00000000000..a2f058c5e14 --- /dev/null +++ b/src/main/resources/bidder-config/floxis.yaml @@ -0,0 +1,25 @@ +adapters: + floxis: + endpoint: https://{Host}.floxis.tech/pbs?seat={SeatId} + endpoint-compression: gzip + ortb-version: "2.6" + modifying-vast-xml-allowed: false + meta-info: + maintainer-email: prebid@floxis.tech + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 1609 + usersync: + cookie-family-name: floxis + redirect: + url: "https://px-us-e.floxis.tech/sync?gdpr={gdpr}&gdpr_consent={gdpr_consent}&gpp={gpp}&gpp_sid={gpp_sid}&us_privacy={us_privacy}&dest={redirect_url}" + uid-macro: "${USER_ID}" diff --git a/src/main/resources/bidder-config/sevio.yaml b/src/main/resources/bidder-config/sevio.yaml new file mode 100644 index 00000000000..6bb520c4648 --- /dev/null +++ b/src/main/resources/bidder-config/sevio.yaml @@ -0,0 +1,13 @@ +adapters: + sevio: + endpoint: https://req.adx.ws/rtb + meta-info: + maintainer-email: technical@sevio.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 1393 diff --git a/src/main/resources/bidder-config/smarthub.yaml b/src/main/resources/bidder-config/smarthub.yaml index 8ed2e05b854..9edde91f97f 100644 --- a/src/main/resources/bidder-config/smarthub.yaml +++ b/src/main/resources/bidder-config/smarthub.yaml @@ -1,45 +1,45 @@ adapters: smarthub: - endpoint: https://prebid.attekmi.com/pbserver?partnerName={Host}&seat={AccountID}&token={SourceId} + endpoint: https://prebid.attekmi.co/pbserver?partnerName={Host}&seat={AccountID}&token={SourceId} aliases: markapp: enabled: false - endpoint: https://markapp-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://markapp-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 jdpmedia: enabled: false - endpoint: https://jdpmedia-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://jdpmedia-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 tredio: enabled: false - endpoint: https://tredio-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://tredio-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 felixads: enabled: false - endpoint: https://felixads-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://felixads-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 jambojar: enabled: false - endpoint: https://jambojar-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://jambojar-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 adinify: enabled: false - endpoint: https://adinify-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://adinify-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 artechnology: enabled: false - endpoint: https://artechnology-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://artechnology-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 addigi: enabled: false - endpoint: https://addigi-prebid.attekmi.com/pbserver/?seat={AccountID}&token={SourceId} + endpoint: https://addigi-prebid.attekmi.co/pbserver/?seat={AccountID}&token={SourceId} meta-info: vendor-id: 0 radiantfusion: diff --git a/src/main/resources/bidder-config/teqblaze.yaml b/src/main/resources/bidder-config/teqblaze.yaml index d6c34012379..4c2c4af5a3c 100644 --- a/src/main/resources/bidder-config/teqblaze.yaml +++ b/src/main/resources/bidder-config/teqblaze.yaml @@ -115,6 +115,12 @@ adapters: meta-info: maintainer-email: prebid@adsmovil.com vendor-id: 0 + appMonstaMedia: + enabled: false + endpoint: https://ssp-us.appmonsta.ai/pserver + meta-info: + maintainer-email: media.support@appmonsta.ai + vendor-id: 1283 meta-info: maintainer-email: github@teqblaze.com app-media-types: diff --git a/src/main/resources/bidder-config/thetradedesk.yaml b/src/main/resources/bidder-config/thetradedesk.yaml index 628e3b149fa..421cf00a91f 100644 --- a/src/main/resources/bidder-config/thetradedesk.yaml +++ b/src/main/resources/bidder-config/thetradedesk.yaml @@ -1,6 +1,7 @@ adapters: thetradedesk: endpoint: https://direct.adsrvr.org/bid/bidder/{SupplyId} + ortb-version: "2.6" aliases: ttd: meta-info: @@ -10,10 +11,12 @@ adapters: app-media-types: - banner - video + - audio - native site-media-types: - banner - video + - audio - native supported-vendors: vendor-id: 21 diff --git a/src/main/resources/static/bidder-params/adswag.json b/src/main/resources/static/bidder-params/adswag.json new file mode 100644 index 00000000000..4a3b70230d1 --- /dev/null +++ b/src/main/resources/static/bidder-params/adswag.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adswag Adapter Params", + "description": "A schema which validates params accepted by the Adswag adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "minLength": 1, + "description": "Adswag publisher id (issued at onboarding)" + }, + "placementId": { + "type": "string", + "minLength": 1, + "description": "Explicit placement override; omit to let Adswag discover the placement from GPID or the ad unit code" + } + }, + "required": ["publisherId"] +} diff --git a/src/main/resources/static/bidder-params/bidwave.json b/src/main/resources/static/bidder-params/bidwave.json new file mode 100644 index 00000000000..d7edfaed1ad --- /dev/null +++ b/src/main/resources/static/bidder-params/bidwave.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "BidWave Adapter Params", + "description": "A schema which validates params accepted by the BidWave adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$", + "description": "Publisher ID" + } + }, + "required": [ + "publisherId" + ] +} diff --git a/src/main/resources/static/bidder-params/connectad.json b/src/main/resources/static/bidder-params/connectad.json index e36410928da..5a6f6ec9294 100644 --- a/src/main/resources/static/bidder-params/connectad.json +++ b/src/main/resources/static/bidder-params/connectad.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "ConnectAd S2S dapter Params", + "title": "ConnectAd S2S Adapter Params", "description": "A schema which validates params accepted by the ConnectAd Adapter", "type": "object", "properties": { @@ -20,7 +20,11 @@ }, "bidfloor": { "type": "number", - "description": "Requests Floorprice" + "description": "Requested Floorprice" + }, + "endpointUrl": { + "type": "string", + "description": "Client-side only (Prebid.js): override the bid endpoint URL for testing or a custom datacenter. Ignored by Prebid Server, which always uses its configured endpoint." } }, "required": [ diff --git a/src/main/resources/static/bidder-params/floxis.json b/src/main/resources/static/bidder-params/floxis.json new file mode 100644 index 00000000000..e0a64d19e87 --- /dev/null +++ b/src/main/resources/static/bidder-params/floxis.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Floxis Adapter Params", + "description": "A schema which validates params accepted by the Floxis adapter", + "type": "object", + "properties": { + "seat": { + "type": "string", + "minLength": 1, + "description": "The Floxis seat ID this publisher buys through" + }, + "region": { + "type": "string", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", + "description": "The Floxis region (DNS label, interpolated into the bidding host); defaults to us-e when omitted" + }, + "partner": { + "type": "string", + "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", + "description": "The white-label partner (DNS label, interpolated into the bidding host); defaults to floxis when omitted" + } + }, + "required": ["seat"] +} diff --git a/src/main/resources/static/bidder-params/missena.json b/src/main/resources/static/bidder-params/missena.json index be5217efdb1..69d0291cc36 100644 --- a/src/main/resources/static/bidder-params/missena.json +++ b/src/main/resources/static/bidder-params/missena.json @@ -13,9 +13,9 @@ "type": "string", "description": "Placement Type (Sticky, Header, ...)" }, - "test": { - "type": "string", - "description": "Test Mode" + "sample": { + "type": ["string", "null"], + "description": "Sample" }, "formats": { "type": "array", diff --git a/src/main/resources/static/bidder-params/sevio.json b/src/main/resources/static/bidder-params/sevio.json new file mode 100644 index 00000000000..da0992cc359 --- /dev/null +++ b/src/main/resources/static/bidder-params/sevio.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Sevio Adapter Params", + "description": "A schema which validates params accepted by the Sevio adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement Id or Zone Id" + } + }, + "required": [ + "placementId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/adswag/AdswagBidderTest.java b/src/test/java/org/prebid/server/bidder/adswag/AdswagBidderTest.java new file mode 100644 index 00000000000..9451efc6247 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/adswag/AdswagBidderTest.java @@ -0,0 +1,555 @@ +package org.prebid.server.bidder.adswag; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Audio; +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.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +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.adswag.ExtImpAdswag; +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.Arrays.asList; +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.assertj.core.api.Assertions.tuple; + +public class AdswagBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/prebid/bid"; + private static final String SERVE_URL = "https://ads.adswag.ai/v1/ad?sc=c2ln&sig=YWJj"; + private static final String VAST_ADM = """ + \ + Adswag\ + \ + """; + private static final String IFRAME_ADM = """ + """; + + private final AdswagBidder target = new AdswagBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AdswagBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp( + imp -> imp.id("imp1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Error parsing imp.ext for impression imp1"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfPublisherIdIsMissing() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").ext(givenImpExt(" ", "placement")))); + + // 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()).isEqualTo("missing publisherId for imp imp1"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfNeitherSiteNorAppIsPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").ext(givenImpExt("pub-1", null)))); + + // 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()).isEqualTo("request must contain either site or app"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldPromotePublisherIdAndRewriteImpExt() { + // given + final ObjectNode impExt = givenImpExt("pub-1", "plc-1"); + impExt.put("gpid", "/1111/homepage#div-1"); + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder() + .publisher(Publisher.builder().name("Example").build()) + .build()), + givenImp(imp -> imp.id("imp1").ext(impExt))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getSite) + .extracting(Site::getPublisher) + .extracting(Publisher::getId, Publisher::getName) + .containsExactly(tuple("pub-1", "Example")); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .allSatisfy(ext -> { + assertThat(ext.has("bidder")).isFalse(); + assertThat(ext.get("gpid").textValue()).isEqualTo("/1111/homepage#div-1"); + assertThat(ext.get("adswag").get("placement_id").textValue()).isEqualTo("plc-1"); + }); + } + + @Test + public void makeHttpRequestsShouldRemoveImpExtWhenOnlyBidderParamsPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().domain("example.nl").build()), + givenImp(imp -> imp.id("imp1").ext(givenImpExt("pub-1", null)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsOnlyNulls(); + } + + @Test + public void makeHttpRequestsShouldPromotePublisherIdToApp() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().bundle("com.example.app").build()), + givenImp(imp -> imp.id("imp1").ext(givenImpExt("pub-1", null)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getApp) + .extracting(App::getPublisher) + .extracting(Publisher::getId) + .containsExactly("pub-1"); + } + + @Test + public void makeHttpRequestsShouldDropImpsWithInvalidExtAndKeepValidOnes() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().domain("example.nl").build()), + givenImp(imp -> imp.id("imp1").ext(givenImpExt("pub-1", null))), + givenImp(imp -> imp.id("imp2").ext(givenImpExt(null, "plc-2")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getMessage()).isEqualTo("missing publisherId for imp imp2")); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp1"); + } + + @Test + public void makeHttpRequestsShouldSplitImpsByPublisherId() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().domain("example.nl").build()), + givenImp(imp -> imp.id("imp-a").ext(givenImpExt("pub-a", null))), + givenImp(imp -> imp.id("imp-b").ext(givenImpExt("pub-b", null))), + givenImp(imp -> imp.id("imp-a-2").ext(givenImpExt("pub-a", null)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(payload -> payload.getSite().getPublisher().getId()) + .containsExactly("pub-a", "pub-b"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(payload -> payload.getImp().stream().map(Imp::getId).toList()) + .containsExactly(List.of("imp-a", "imp-a-2"), List.of("imp-b")); + } + + @Test + public void makeBidsShouldReturnEmptyResultForEmptyBody() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, ""), + null); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, "invalid_json"), + null); + + // 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 makeBidsShouldSynthesizeIframeMarkupForBannerServeUrlBid() throws JsonProcessingException { + // given + final Bid responseBid = givenServeUrlBid("imp1"); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("imp1").banner(givenBanner()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType, BidderBid::getBidCurrency) + .containsExactly(tuple(BidType.banner, "EUR")); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm, Bid::getW, Bid::getH, Bid::getMtype) + .containsExactly(tuple(IFRAME_ADM, 300, 250, 1)); + } + + @Test + public void makeBidsShouldPassThroughAdmWhenMarkupIsPresent() throws JsonProcessingException { + // given + final String loaderAdm = ""; + final Bid responseBid = givenServeUrlBid("imp1").toBuilder().adm(loaderAdm).build(); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("imp1").banner(givenBanner()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm) + .containsExactly(loaderAdm); + } + + @Test + public void makeBidsShouldReturnVideoBidForVastMarkup() throws JsonProcessingException { + // given + final Bid responseBid = givenVastBid("imp1"); + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").video(Video.builder().w(640).h(360).build()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType) + .containsExactly(BidType.video); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getAdm, Bid::getW, Bid::getH, Bid::getMtype) + .containsExactly(tuple(VAST_ADM, 640, 360, 2)); + } + + @Test + public void makeBidsShouldReturnAudioBidForVastMarkupOnAudioImp() throws JsonProcessingException { + // given + final Bid responseBid = givenVastBid("imp1"); + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1") + .banner(givenBanner()) + .audio(Audio.builder().mimes(singletonList("audio/mpeg")).build()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType) + .containsExactly(BidType.audio); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getMtype) + .containsExactly(3); + } + + @Test + public void makeBidsShouldResolveServeUrlBidOnMultiFormatImpAsBanner() throws JsonProcessingException { + // given + final Bid responseBid = givenServeUrlBid("imp1"); + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").banner(givenBanner()).video(Video.builder().w(640).h(360).build()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner); + } + + @Test + public void makeBidsShouldSetNurlForVideoServeUrlBidWithoutAdm() throws JsonProcessingException { + // given + final Bid responseBid = givenServeUrlBid("imp1"); + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1").video(Video.builder().w(640).h(360).build()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getMtype) + .containsExactly(tuple(SERVE_URL, 2)); + } + + @Test + public void makeBidsShouldHonorMtypeWhenPresent() throws JsonProcessingException { + // given + final Bid responseBid = givenVastBid("imp1").toBuilder().mtype(2).build(); + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1") + .audio(Audio.builder().mimes(singletonList("audio/mpeg")).build()) + .video(Video.builder().w(640).h(360).build()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType) + .containsExactly(BidType.video); + } + + @Test + public void makeBidsShouldReturnErrorForUnsupportedMtype() throws JsonProcessingException { + // given + final Bid responseBid = givenVastBid("imp1").toBuilder().mtype(4).build(); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("imp1").banner(givenBanner()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).isEqualTo("unsupported bid.mtype 4 for impression imp1"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorForUnknownImp() throws JsonProcessingException { + // given + final Bid responseBid = givenServeUrlBid("unknown-imp"); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("imp1").banner(givenBanner()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()) + .isEqualTo("bid adswag-unknown-imp references unknown imp unknown-imp"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorForBidWithoutAdmOrServeUrl() throws JsonProcessingException { + // given + final Bid responseBid = Bid.builder() + .id("adswag-imp1") + .impid("imp1") + .price(BigDecimal.valueOf(2.5)) + .build(); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("imp1").banner(givenBanner()))); + final BidderCall httpCall = givenHttpCall(bidRequest, singletonList(responseBid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()) + .isEqualTo("bid adswag-imp1 has neither adm nor ext.adswag.serve_url"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldDefaultCurrencyToEur() throws JsonProcessingException { + // given + final BidResponse bidResponse = BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(singletonList(givenServeUrlBid("imp1"))).build())) + .build(); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("imp1").banner(givenBanner()))); + final BidderCall httpCall = BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, mapper.writeValueAsString(bidResponse)), + null); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBidCurrency) + .containsExactly("EUR"); + } + + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { + + return bidRequestCustomizer.apply(BidRequest.builder().imp(asList(imps))).build(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return givenBidRequest(identity(), imps); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder()).build(); + } + + private static ObjectNode givenImpExt(String publisherId, String placementId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpAdswag.of(publisherId, placementId))); + } + + private static Banner givenBanner() { + return Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build(); + } + + private static Bid givenServeUrlBid(String impId) { + final ObjectNode bidExt = mapper.createObjectNode(); + bidExt.putObject("adswag").put("serve_url", SERVE_URL); + return Bid.builder() + .id("adswag-" + impId) + .impid(impId) + .price(BigDecimal.valueOf(2.5)) + .crid("cr-1") + .ext(bidExt) + .build(); + } + + private static Bid givenVastBid(String impId) { + return Bid.builder() + .id("adswag-" + impId) + .impid(impId) + .price(BigDecimal.valueOf(2.5)) + .crid("cr-1") + .adm(VAST_ADM) + .build(); + } + + private static BidResponse givenBidResponse(List bids) { + return BidResponse.builder() + .cur("EUR") + .seatbid(singletonList(SeatBid.builder().bid(bids).build())) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, List bids) + throws JsonProcessingException { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, mapper.writeValueAsString(givenBidResponse(bids))), + null); + } +} diff --git a/src/test/java/org/prebid/server/bidder/bidwave/BidwaveBidderTest.java b/src/test/java/org/prebid/server/bidder/bidwave/BidwaveBidderTest.java new file mode 100644 index 00000000000..43b814120c0 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/bidwave/BidwaveBidderTest.java @@ -0,0 +1,318 @@ +package org.prebid.server.bidder.bidwave; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +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.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bidwave.ExtImpBidwave; + +import java.math.BigDecimal; +import java.util.List; + +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.api.BDDAssertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; + +@ExtendWith(MockitoExtension.class) +public class BidwaveBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://rtb.bidwave.net/rtb/v1/bid"; + private static final String PUBLISHER_ID = "11111111-1111-4111-8111-111111111111"; + private static final String OTHER_PUBLISHER_ID = "22222222-2222-4222-8222-222222222222"; + + @Mock + private CurrencyConversionService currencyConversionService; + + private BidwaveBidder target; + + @BeforeEach + public void setUp() { + target = new BidwaveBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new BidwaveBidder("invalid_url", currencyConversionService, jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldSetBidwaveExt() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt(PUBLISHER_ID)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final BidRequest payload = result.getValue().getFirst().getPayload(); + + assertThat(payload.getExt().getProperty("bidwave").get("pid").asText()).isEqualTo(PUBLISHER_ID); + } + + @Test + public void makeHttpRequestsShouldSetDefaultCurrency() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt(PUBLISHER_ID)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final BidRequest payload = result.getValue().getFirst().getPayload(); + + assertThat(payload.getCur()).containsExactly("USD"); + } + + @Test + public void makeHttpRequestsShouldGroupImpsByPublisherId() { + // given + final Imp firstImp = givenImp(imp -> imp.id("imp-1").ext(givenImpExt(PUBLISHER_ID))); + final Imp secondImp = givenImp(imp -> imp.id("imp-2").ext(givenImpExt(OTHER_PUBLISHER_ID))); + final Imp thirdImp = givenImp(imp -> imp.id("imp-3").ext(givenImpExt(PUBLISHER_ID))); + final BidRequest bidRequest = givenBidRequest(firstImp, secondImp, thirdImp); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2); + assertThat(result.getValue().get(0).getPayload()) + .satisfies(payload -> { + assertThat(payload.getImp()).containsExactly(firstImp, thirdImp); + assertThat(payload.getExt().getProperty("bidwave").get("pid").asText()) + .isEqualTo(PUBLISHER_ID); + }); + assertThat(result.getValue().get(1).getPayload()) + .satisfies(payload -> { + assertThat(payload.getImp()).containsExactly(secondImp); + assertThat(payload.getExt().getProperty("bidwave").get("pid").asText()) + .isEqualTo(OTHER_PUBLISHER_ID); + }); + } + + @Test + public void makeHttpRequestsShouldNormalizeCurrency() { + // given + final Imp imp = givenImp(impBuilder -> impBuilder.id("imp-1").ext(givenImpExt(PUBLISHER_ID))); + final BidRequest bidRequest = givenBidRequest(imp) + .toBuilder() + .cur(List.of("EUR")) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getPayload().getCur()).containsExactly("USD"); + } + + @Test + public void makeHttpRequestsShouldConvertBidFloorCurrency() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .id("imp-1") + .bidfloor(BigDecimal.ONE) + .bidfloorcur("EUR") + .ext(givenImpExt(PUBLISHER_ID)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.TEN, "USD")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenBidFloorCurrencyCannotBeConverted() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willThrow(new PreBidException("missing conversion rate")); + + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp + .id("imp-1") + .bidfloor(BigDecimal.ONE) + .bidfloorcur("EUR") + .ext(givenImpExt(PUBLISHER_ID)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly(BidderError.badInput( + "expected currency USD for bid floor; unable to convert from EUR for imp `imp-1`")); + } + + @Test + public void makeBidsShouldReturnBannerAndVideoBids() throws JsonProcessingException { + // given + final Bid bannerBid = Bid.builder().impid("imp-1").mtype(1).build(); + final Bid videoBid = Bid.builder().impid("imp-2").mtype(2).build(); + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp-1").banner(givenBanner()).ext(givenImpExt(PUBLISHER_ID))), + givenImp(imp -> imp.id("imp-2").video(givenVideo()).ext(givenImpExt(PUBLISHER_ID)))); + final BidderCall httpCall = sampleHttpCall( + bidRequest, + mapper.writeValueAsString(givenBidResponse(bannerBid, videoBid))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly( + BidderBid.of(bannerBid, banner, "USD"), + BidderBid.of(videoBid, video, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeIsAbsent() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().impid("imp-1").build(); + final BidderCall httpCall = sampleHttpCall( + givenBidRequest(), + mapper.writeValueAsString(givenBidResponse(bid))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse( + "Bid must have non-zero MType for impression with ID: \"imp-1\"")); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().impid("imp-1").mtype(3).build(); + final BidderCall httpCall = sampleHttpCall( + givenBidRequest(givenImp(imp -> imp.id("imp-1").banner(givenBanner()).ext(givenImpExt(PUBLISHER_ID)))), + mapper.writeValueAsString(givenBidResponse(bid))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse("Unsupported MType 3 for impression with ID: \"imp-1\"")); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = sampleHttpCall(givenBidRequest(), "invalid response"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode: Unrecognized token")); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder() + .id("request-id") + .site(Site.builder().domain("publisher.example.com").build()) + .device(Device.builder() + .ip("203.0.113.42") + .geo(Geo.builder().country("USA").region("CA").city("San Francisco").build()) + .build()) + .imp(List.of(imps)) + .build(); + } + + private static BidRequest givenBidRequest() { + return givenBidRequest(givenImp(imp -> imp.id("imp-1").banner(givenBanner()).ext(givenImpExt(PUBLISHER_ID)))); + } + + private static Imp givenImp(java.util.function.Function impCustomizer) { + return impCustomizer.apply(Imp.builder()).build(); + } + + private static Banner givenBanner() { + return Banner.builder() + .format(singletonList(Format.builder().w(300).h(250).build())) + .build(); + } + + private static Video givenVideo() { + return Video.builder() + .mimes(singletonList("video/mp4")) + .protocols(List.of(2, 3)) + .w(640) + .h(360) + .build(); + } + + private static com.fasterxml.jackson.databind.node.ObjectNode givenImpExt(String publisherId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpBidwave.of(publisherId))); + } + + private static BidResponse givenBidResponse(Bid... bids) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build(); + } + + private static BidderCall sampleHttpCall(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/connectad/ConnectAdBidderTest.java b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java index 3ac89edc67a..122f4ffad8e 100644 --- a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java @@ -49,6 +49,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .id("123") + .banner(Banner.builder().w(300).h(250).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) .build())) .build(); @@ -109,12 +110,15 @@ public void makeBidsShouldReturnEmptyListWhenBidResponseSeatBidIsNull() throws J @Test public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(); final BidderCall httpCall = givenHttpCall( - BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + bidRequest, mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when - final Result> result = target.makeBids(httpCall, null); + final Result> result = target.makeBids(httpCall, bidRequest); // then assertThat(result.getErrors()).isEmpty(); @@ -146,7 +150,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtHasNoSiteId() { impBuilder -> impBuilder .id("123") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of("12", null, BigDecimal.ONE))))); + ExtImpConnectAd.of(12, null, BigDecimal.ONE))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -164,7 +168,7 @@ public void impSecureShouldBeOneIfSitePageStartsFromHttps() { impBuilder -> impBuilder .id("123") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of("12", "1", BigDecimal.ONE))))); + ExtImpConnectAd.of(12, 1, BigDecimal.ONE))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -180,6 +184,32 @@ public void impSecureShouldBeOneIfSitePageStartsFromHttps() { .containsOnly(1); } + @Test + public void makeHttpRequestsShouldPropagateSiteIdAndNetworkId() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpConnectAd.of(12345, 67890, BigDecimal.ONE))))); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(1) + .first() + .satisfies(imp -> { + assertThat(imp.getTagid()).isEqualTo("67890"); + assertThat(imp.getExt().get("networkId").asInt()).isEqualTo(12345); + assertThat(imp.getExt().get("siteId").asInt()).isEqualTo(67890); + }); + } + private static BidRequest givenBidRequest( Function bidRequestCustomizer, Function impCustomizer) { @@ -202,7 +232,7 @@ private static Imp givenImp(Function impCustomiz .w(14) .h(15).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of("12", "12", BigDecimal.ONE))))) + ExtImpConnectAd.of(12, 12, BigDecimal.ONE))))) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java new file mode 100644 index 00000000000..f94d035289f --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -0,0 +1,595 @@ +package org.prebid.server.bidder.floxis; + +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.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +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.floxis.ExtImpFloxis; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +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.assertj.core.api.Assertions.tuple; + +public class FloxisBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://{Host}.floxis.tech/pbs?seat={SeatId}"; + + private final FloxisBidder target = new FloxisBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FloxisBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp + .id("imp-1") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("invalid imp.ext.bidder for imp imp-1:"); + }); + } + + @Test + public void makeHttpRequestsShouldUrlEscapeSeatAndUseEuHost() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("a b&c", "eu"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://eu.floxis.tech/pbs?seat=a%20b%26c"); + } + + @Test + public void makeHttpRequestsShouldUseApacHost() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("seat-apac", "apac"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://apac.floxis.tech/pbs?seat=seat-apac"); + } + + @Test + public void makeHttpRequestsShouldUseUseHostForExplicitUsE() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldDefaultToUseHostWhenRegionMissing() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", null))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldUseArbitraryValidRegionLabelAsSubdomain() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "mars"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://mars.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldUsePartnerPrefixedHostWhenPartnerProvided() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "us-e", "acme"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://acme-us-e.floxis.tech/pbs?seat=abc"); + } + + @Test + public void makeHttpRequestsShouldLeaveEndpointUnchangedWhenItDeclaresNoMacros() { + // given + final FloxisBidder bidder = new FloxisBidder("http://localhost:8090/floxis-exchange", jacksonMapper); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "eu"))); + + // when + final Result>> result = bidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("http://localhost:8090/floxis-exchange"); + } + + @Test + public void makeHttpRequestsShouldRouteOnceAndForwardAllImpsWhenSeatAndRegionMatch() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "eu"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://eu.floxis.tech/pbs?seat=seat-eu"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp-1", "imp-2"); + } + + @Test + public void makeHttpRequestsShouldRouteOnceWhenImpsResolveToSameHostThroughDefaults() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-us", null))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-us", "us-e", "floxis"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://us-e.floxis.tech/pbs?seat=seat-us"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp-1", "imp-2"); + } + + @Test + public void makeHttpRequestsShouldRouteOnceWhenRegionCaseDiffers() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "EU"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://eu.floxis.tech/pbs?seat=seat-eu"); + } + + @Test + public void makeHttpRequestsShouldSplitPerSeatWhenImpsShareAHost() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-other", "eu"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getUri) + .containsExactly( + "https://eu.floxis.tech/pbs?seat=seat-eu", + "https://eu.floxis.tech/pbs?seat=seat-other"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp-1", "imp-2"); + } + + @Test + public void makeHttpRequestsShouldSplitPerRegionWhenImpsTargetDifferentRegions() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "apac"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getUri) + .containsExactly( + "https://eu.floxis.tech/pbs?seat=seat-eu", + "https://apac.floxis.tech/pbs?seat=seat-eu"); + } + + @Test + public void makeHttpRequestsShouldSplitPerPartnerWhenImpsTargetDifferentPartners() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-1").ext(givenImpExt("seat-eu", "eu", "acme"))), + givenImp(imp -> imp.id("imp-2").ext(givenImpExt("seat-eu", "eu", "other"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getUri) + .containsExactly( + "https://acme-eu.floxis.tech/pbs?seat=seat-eu", + "https://other-eu.floxis.tech/pbs?seat=seat-eu"); + } + + @Test + public void makeHttpRequestsShouldDropOnlyTheImpWhoseExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("req-1") + .imp(asList( + givenImp(imp -> imp.id("imp-bad") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))), + givenImp(imp -> imp.id("imp-good").ext(givenImpExt("seat-eu", "eu"))))) + .site(Site.builder().id("271").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getMessage()) + .startsWith("invalid imp.ext.bidder for imp imp-bad:")); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://eu.floxis.tech/pbs?seat=seat-eu"); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("imp-good"); + } + + @Test + public void makeHttpRequestsShouldForwardRequestBodyUnchanged() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "eu"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsExactly(bidRequest); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), "invalid"); + + // when + final Result> result = target.makeBids(httpCall, BidRequest.builder().build()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response)); + } + + @Test + public void makeBidsShouldReturnEmptyWhenResponseHasNoSeatBid() throws Exception { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, BidRequest.builder().build()); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldSkipNullSeatBidAndEmptyBids() throws Exception { + // given + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(asList( + null, + SeatBid.builder().bid(null).build(), + SeatBid.builder() + .bid(singletonList(Bid.builder().impid("imp-1").mtype(1).build())) + .build())) + .build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner); + } + + @Test + public void makeBidsShouldResolveBidTypeFromMTypeAndPassCurrency() throws Exception { + // given + final BidResponse bidResponse = BidResponse.builder() + .cur("EUR") + .seatbid(singletonList(SeatBid.builder() + .bid(asList( + Bid.builder().impid("imp-1").mtype(1).build(), + Bid.builder().impid("imp-1").mtype(2).build(), + Bid.builder().impid("imp-1").mtype(3).build(), + Bid.builder().impid("imp-1").mtype(4).build())) + .build())) + .build(); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType, BidderBid::getBidCurrency) + .containsExactly( + tuple(BidType.banner, "EUR"), + tuple(BidType.video, "EUR"), + tuple(BidType.audio, "EUR"), + tuple(BidType.xNative, "EUR")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMTypeUnsupported() throws Exception { + // given + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("imp-1").mtype(7).build()); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unsupported bid.mtype 7 for impression imp-1")); + } + + @Test + public void makeBidsShouldResolveBidTypeFromSingleFormatImpWhenMTypeAbsent() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList( + Imp.builder().id("b").banner(Banner.builder().build()).build(), + Imp.builder().id("v").video(Video.builder().build()).build(), + Imp.builder().id("a").audio(Audio.builder().build()).build(), + Imp.builder().id("n").xNative(Native.builder().build()).build())) + .build(); + final BidResponse bidResponse = BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(asList( + Bid.builder().impid("b").build(), + Bid.builder().impid("v").build(), + Bid.builder().impid("a").build(), + Bid.builder().impid("n").build())) + .build())) + .build(); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner, BidType.video, BidType.audio, BidType.xNative); + } + + @Test + public void makeBidsShouldReturnErrorForMultiFormatImpWithoutMType() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("imp-1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .build())) + .build(); + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("imp-1").build()); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unable to resolve a single media type for impression imp-1; set bid.mtype")); + } + + @Test + public void makeBidsShouldReturnErrorForImpWithoutFormatAndWithoutMType() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().id("imp-1").build())) + .build(); + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("imp-1").build()); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unable to resolve a single media type for impression imp-1; set bid.mtype")); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidImpIdNotFound() throws Exception { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().id("imp-1").banner(Banner.builder().build()).build())) + .build(); + final BidResponse bidResponse = givenBidResponse(Bid.builder().impid("no-such-imp").build()); + final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse( + "unable to find impression no-such-imp for bid")); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return BidRequest.builder() + .id("req-1") + .imp(singletonList(givenImp(impCustomizer))) + .site(Site.builder().id("271").build()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("imp-1") + .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build()) + .ext(givenImpExt("pub-seat-1", null))) + .build(); + } + + private static ObjectNode givenImpExt(String seat, String region) { + return givenImpExt(seat, region, null); + } + + private static ObjectNode givenImpExt(String seat, String region, String partner) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpFloxis.of(seat, region, partner))); + } + + private static BidResponse givenBidResponse(Bid bid) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid)).build())) + .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/missena/MissenaBidderTest.java b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java index b4025692372..441e1700023 100644 --- a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java @@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; 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.SupplyChain; +import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import org.junit.jupiter.api.BeforeEach; @@ -33,7 +35,6 @@ import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.UnaryOperator; @@ -41,6 +42,7 @@ 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.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -57,7 +59,8 @@ class MissenaBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test-url.com/?t={PublisherID}"; - private static final String TEST_PBS_VERSION = "pbs-java/1.0"; + private static final String TEST_PBS_VERSION_RECORD = "pbs-java/1.0"; + private static final String TEST_EXPECTED_VERSION = "prebid-server-java@1.0"; @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; @@ -75,7 +78,7 @@ public void setUp() { currencyConversionService, prebidVersionProvider); - given(prebidVersionProvider.getNameVersionRecord()).willReturn(TEST_PBS_VERSION); + given(prebidVersionProvider.getNameVersionRecord()).willReturn(TEST_PBS_VERSION_RECORD); given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) .willAnswer(invocation -> invocation.getArgument(0)); } @@ -83,7 +86,7 @@ public void setUp() { @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenInvalidImpExt())); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(givenInvalidImpExt()))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -98,20 +101,26 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { public void makeHttpRequestsShouldMakeRequestForFirstValidImp() { // given final ObjectNode settingsNode = mapper.createObjectNode().put("settingKey", "settingValue"); + final List userEids = List.of(Eid.builder() + .source("id-source") + .uids(List.of(Uid.builder().id("uid").atype(1).build())) + .build()); final BidRequest bidRequest = BidRequest.builder() .id("requestId") + .test(1) .tmax(500L) .cur(singletonList("USD")) .imp(List.of( givenImp(imp -> imp.id("impId1") - .ext(givenImpExt("apiKey1", "placementId1", "1", List.of("banner"), settingsNode))), + .ext(givenImpExt("apiKey1", "placementId1", "sample1", + List.of("banner"), settingsNode))), givenImp(imp -> imp.id("impId2") - .ext(givenImpExt("apiKey2", "placementId2", "0", null, null))))) + .ext(givenImpExt("apiKey2", "placementId2", "sample2", null, null))))) .site(Site.builder().page("http://test.com/page").domain("test.com").build()) .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) .user(User.builder().buyeruid("buyer1") - .ext(ExtUser.builder().consent("consentStr").build()).build()) + .ext(ExtUser.builder().consent("consentStr").eids(userEids).build()).build()) .source(Source.builder().schain(SupplyChain.of(1, null, null, null)).build()) .device(Device.builder().ua("test-ua").ip("123.123.123.123").build()) .build(); @@ -121,22 +130,25 @@ public void makeHttpRequestsShouldMakeRequestForFirstValidImp() { // then final MissenaUserParams expectedUserParams = MissenaUserParams.builder() + .apiKey("apiKey1") .formats(List.of("banner")) .placement("placementId1") - .testMode("1") + .sample("sample1") .settings(settingsNode) .build(); final MissenaAdRequest expectedPayload = MissenaAdRequest.builder() .adUnit("impId1") .currency("USD") + .debug(true) + .userEids(userEids) .floor(BigDecimal.valueOf(0.1)) .floorCurrency("USD") .idempotencyKey("requestId") .requestId("requestId") .timeout(500L) .params(expectedUserParams) - .version(TEST_PBS_VERSION) + .version(TEST_EXPECTED_VERSION) .bidRequest(bidRequest) .build(); @@ -149,12 +161,140 @@ public void makeHttpRequestsShouldMakeRequestForFirstValidImp() { .containsExactly(Collections.singleton("impId1")); } + @Test + public void makeHttpRequestsShouldPassOriginalBidRequestAsOrtb2() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getBidRequest) + .isEqualTo(bidRequest); + } + + @Test + public void makeHttpRequestsShouldPassUserEidsWhenPresent() { + // given + final List userEids = List.of(Eid.builder() + .source("id-source") + .uids(List.of(Uid.builder().id("uid").atype(1).build())) + .build()); + final BidRequest bidRequest = givenBidRequest( + request -> request.user(User.builder().ext(ExtUser.builder().eids(userEids).build()).build()), + givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getUserEids) + .isEqualTo(userEids); + } + + @Test + public void makeHttpRequestsShouldNotPassUserEidsWhenUserIsNull() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.user(null), + givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getUserEids) + .isNull(); + } + + @Test + public void makeHttpRequestsShouldNotPassUserEidsWhenEidsAreEmpty() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.user( + User.builder().ext(ExtUser.builder().eids(Collections.emptyList()).build()).build()), + givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getUserEids) + .isNull(); + } + + @Test + public void makeHttpRequestsShouldSetDebugWhenTestIsOne() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.test(1), + givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getDebug) + .isEqualTo(true); + } + + @Test + public void makeHttpRequestsShouldNotSetDebugWhenTestIsNotOne() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.test(0), + givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getDebug) + .isNull(); + } + + @Test + public void makeHttpRequestsShouldSendUnknownVersionWhenVersionRecordIsNull() { + // given + given(prebidVersionProvider.getNameVersionRecord()).willReturn(null); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(givenImpExt("apiKey")))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getPayload) + .extracting(MissenaAdRequest::getVersion) + .isEqualTo("prebid-server-java@unknown"); + } + @Test public void makeHttpRequestsShouldReturnErrorIfAllImpsAreInvalid() { // given final BidRequest bidRequest = givenBidRequest( - imp -> imp.ext(givenInvalidImpExt()), - imp -> imp.ext(givenInvalidImpExt())); + givenImp(imp -> imp.ext(givenInvalidImpExt())), + givenImp(imp -> imp.ext(givenInvalidImpExt()))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -169,11 +309,11 @@ public void makeHttpRequestsShouldReturnErrorIfAllImpsAreInvalid() { @Test public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpAndIpv6() { // given - final BidRequest bidRequest = givenBidRequest(identity()) - .toBuilder() - .site(Site.builder().page("http://page.com").build()) - .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) - .build(); + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(Site.builder().page("http://page.com").build()) + .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()), + givenImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -195,10 +335,9 @@ public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpAndIpv6() @Test public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpv6Only() { // given - final BidRequest bidRequest = givenBidRequest(identity()) - .toBuilder() - .device(Device.builder().ip(null).ipv6("ipv6").build()) - .build(); + final BidRequest bidRequest = givenBidRequest( + request -> request.device(Device.builder().ip(null).ipv6("ipv6").build()), + givenImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -244,7 +383,7 @@ public void makeHttpRequestsShouldConvertBidFloorCurrency() { @Test public void makeHttpRequestsShouldUseCorrectUri() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("testApiKey"))); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.ext(givenImpExt("testApiKey")))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -260,7 +399,7 @@ public void makeHttpRequestsShouldUseCorrectUri() { public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given final BidderCall httpCall = givenHttpCall("invalid"); - final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1")); + final BidRequest bidRequest = givenBidRequest(givenImp(imp -> imp.id("impId1"))); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -281,10 +420,14 @@ public void makeBidsShouldReturnSingleBid() throws JsonProcessingException { .cpm(BigDecimal.TEN) .currency("USD") .ad("adm") + .width(320) + .height(100) .build(); final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); - final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId")).toBuilder().id("requestId").build(); + final BidRequest bidRequest = givenBidRequest( + request -> request.id("requestId"), + givenImp(imp -> imp.id("impId"))); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -298,17 +441,48 @@ public void makeBidsShouldReturnSingleBid() throws JsonProcessingException { .price(BigDecimal.TEN) .adm("adm") .crid("id") + .w(320) + .h(100) .build(); assertThat(result.getValue()).hasSize(1) .containsOnly(BidderBid.of(expectedBid, BidType.banner, "USD")); } - private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { - final List imps = Arrays.stream(impCustomizers) - .map(MissenaBidderTest::givenImp) - .toList(); - return BidRequest.builder().imp(imps).cur(singletonList("USD")).build(); + @Test + public void makeBidsShouldReturnBidWithoutSizeWhenResponseHasNoSize() throws JsonProcessingException { + // given + final MissenaAdResponse bidResponse = MissenaAdResponse.builder() + .requestId("id") + .cpm(BigDecimal.TEN) + .currency("USD") + .ad("adm") + .build(); + + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + final BidRequest bidRequest = givenBidRequest( + request -> request.id("requestId"), + givenImp(imp -> imp.id("impId"))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getW, Bid::getH) + .containsExactly(tuple(null, null)); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return givenBidRequest(identity(), imps); + } + + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + Imp... imps) { + + return bidRequestCustomizer.apply(BidRequest.builder().imp(List.of(imps)).cur(singletonList("USD"))).build(); } private static Imp givenImp(UnaryOperator impCustomizer) { @@ -326,14 +500,14 @@ private static ObjectNode givenImpExt(String apiKey) { private static ObjectNode givenImpExt(String apiKey, String placement, - String testMode, + String sample, List formats, ObjectNode settings) { final ExtImpMissena extImpMissena = ExtImpMissena.builder() .apiKey(apiKey) .placement(placement) - .testMode(testMode) + .sample(sample) .formats(formats) .settings(settings) .build(); diff --git a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java index 7a4e483303c..9a354cfbae7 100644 --- a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; 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.Deal; @@ -189,10 +190,10 @@ public void makeHttpRequestsShouldNotReturnErrorIfNativePresent() { } @Test - public void makeHttpRequestsShouldCreateSiteAndPublisherWhenBidRequestHasNoSite() { + public void makeHttpRequestsShouldNotCreateSiteWhenBidRequestHasAppOnly() { // given final BidRequest bidRequest = givenBidRequest( - bidReq -> bidReq.site(null), + bidReq -> bidReq.site(null).app(App.builder().id("app_id").build()), identity(), identity()); @@ -204,12 +205,31 @@ public void makeHttpRequestsShouldCreateSiteAndPublisherWhenBidRequestHasNoSite( assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) - .allSatisfy(site -> { - assertThat(site).isNotNull(); - assertThat(site.getPublisher()).isNotNull(); - assertThat(site.getPublisher().getExt()).isNotNull(); + .containsOnlyNulls(); + } - final JsonNode prebidNode = site.getPublisher().getExt().getProperty("prebid"); + @Test + public void makeHttpRequestsShouldAddPublisherToAppWhenBidRequestHasAppOnly() { + // given + final BidRequest bidRequest = givenBidRequest( + bidReq -> bidReq.site(null).app(App.builder().id("app_id").build()), + identity(), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getApp) + .allSatisfy(app -> { + assertThat(app.getId()).isEqualTo("app_id"); + assertThat(app.getPublisher()).isNotNull(); + assertThat(app.getPublisher().getExt()).isNotNull(); + + final JsonNode prebidNode = app.getPublisher().getExt().getProperty("prebid"); assertThat(prebidNode).isNotNull(); assertThat(prebidNode.get("publisherId").asText()).isEqualTo("publisherId"); }); @@ -236,8 +256,10 @@ public void makeHttpRequestsShouldAddPublisherToExistingSiteWhenNoPublisher() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .allSatisfy(site -> { + .allSatisfy(payload -> { + assertThat(payload.getApp()).isNull(); + + final Site site = payload.getSite(); assertThat(site.getId()).isEqualTo("site_id"); assertThat(site.getName()).isEqualTo("site_name"); assertThat(site.getDomain()).isEqualTo("example.com"); diff --git a/src/test/java/org/prebid/server/bidder/sevio/SevioBidderTest.java b/src/test/java/org/prebid/server/bidder/sevio/SevioBidderTest.java new file mode 100644 index 00000000000..8a026f91ae4 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/sevio/SevioBidderTest.java @@ -0,0 +1,219 @@ +package org.prebid.server.bidder.sevio; + +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 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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +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.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class SevioBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://req.adx.ws/rtb"; + + private final SevioBidder target = new SevioBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new SevioBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedRequestWithAllImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1"), + imp -> imp.id("impId2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(bidRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(bidRequest)) + .satisfies(request -> assertThat(request.getImpIds()) + .containsExactlyInAnyOrder("impId1", "impId2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1"), + imp -> imp.id("impId2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + 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)) + .satisfies(headers -> assertThat(headers.get(X_OPENRTB_VERSION_HEADER)) + .isEqualTo("2.6")); + } + + @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()).hasSize(1).first() + .extracting(HttpRequest::getUri) + .isEqualTo(ENDPOINT_URL); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("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 makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(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 makeBidsShouldReturnBannerBidWhenMtypeIsBanner() throws JsonProcessingException { + // given + final Bid bannerBid = Bid.builder().impid("1").mtype(1).build(); + + 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, banner, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidWhenMtypeIsNative() throws JsonProcessingException { + // given + final Bid nativeBid = Bid.builder().impid("4").mtype(4).build(); + + 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, xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeIsNotSupported() throws JsonProcessingException { + // given + final Bid bannerBid = Bid.builder().impid("id").mtype(1).build(); + final Bid audioBid = Bid.builder().impid("id").mtype(3).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid, audioBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse("failed to parse bid mtype (3) for impression id \"id\"")); + assertThat(result.getValue()).containsOnly(BidderBid.of(bannerBid, banner, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeIsMissing() throws JsonProcessingException { + // given + final Bid bid = Bid.builder().id("bidId").impid("1").build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse("Missing MType for bid: bidId")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(SevioBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).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/thetradedesk/TheTradeDeskBidderTest.java b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java index 46db30df08c..06e6c25817a 100644 --- a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java @@ -35,6 +35,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; @@ -429,6 +430,21 @@ public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); } + @Test + public void makeBidsShouldReturnAudioBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(3).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(3).build(), audio, "USD")); + } + @Test public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given @@ -483,7 +499,7 @@ public void makeBidsShouldReturnValidBidsAndErrorsForMixedMediaTypes() throws Js .seatbid(singletonList(SeatBid.builder() .bid(Arrays.asList( Bid.builder().mtype(1).impid("valid1").build(), // valid banner - Bid.builder().mtype(3).impid("invalid1").build(), // invalid mtype + Bid.builder().mtype(5).impid("invalid1").build(), // invalid mtype Bid.builder().mtype(2).impid("valid2").build(), // valid video Bid.builder().mtype(null).impid("invalid2").build() // null mtype )) diff --git a/src/test/java/org/prebid/server/it/AdswagTest.java b/src/test/java/org/prebid/server/it/AdswagTest.java new file mode 100644 index 00000000000..a80748e57ad --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdswagTest.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 AdswagTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdswag() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adswag-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adswag/test-adswag-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adswag/test-adswag-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adswag/test-auction-adswag-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adswag/test-auction-adswag-response.json", response, singletonList("adswag")); + } +} diff --git a/src/test/java/org/prebid/server/it/AppMonstaMediaTest.java b/src/test/java/org/prebid/server/it/AppMonstaMediaTest.java new file mode 100644 index 00000000000..2c00ea3362f --- /dev/null +++ b/src/test/java/org/prebid/server/it/AppMonstaMediaTest.java @@ -0,0 +1,36 @@ +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 AppMonstaMediaTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAppMonstaMedia() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appMonstaMedia-exchange/")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/appMonstaMedia/test-appMonstaMedia-bid-request.json"))) + .willReturn(aResponse() + .withBody(jsonFrom("openrtb2/appMonstaMedia/test-appMonstaMedia-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/appMonstaMedia/test-auction-appMonstaMedia-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/appMonstaMedia/test-auction-appMonstaMedia-response.json", response, + List.of("appMonstaMedia")); + } + +} diff --git a/src/test/java/org/prebid/server/it/BidwaveTest.java b/src/test/java/org/prebid/server/it/BidwaveTest.java new file mode 100644 index 00000000000..3ad87828e83 --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidwaveTest.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 BidwaveTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBidwave() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidwave-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bidwave/test-bidwave-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bidwave/test-bidwave-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidwave/test-auction-bidwave-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bidwave/test-auction-bidwave-response.json", response, singletonList("bidwave")); + } +} diff --git a/src/test/java/org/prebid/server/it/FloxisTest.java b/src/test/java/org/prebid/server/it/FloxisTest.java new file mode 100644 index 00000000000..429948bfb45 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FloxisTest.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 FloxisTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromFloxis() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/floxis-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/floxis/test-floxis-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floxis/test-floxis-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/floxis/test-auction-floxis-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/floxis/test-auction-floxis-response.json", response, singletonList("floxis")); + } +} diff --git a/src/test/java/org/prebid/server/it/SevioTest.java b/src/test/java/org/prebid/server/it/SevioTest.java new file mode 100644 index 00000000000..d3cb42b7e53 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SevioTest.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 SevioTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSevio() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/sevio-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/sevio/test-sevio-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/sevio/test-sevio-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/sevio/test-auction-sevio-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/sevio/test-auction-sevio-response.json", + response, + singletonList("sevio")); + } + +} diff --git a/src/test/java/org/prebid/server/validation/ImpValidatorTest.java b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java index bbc329fbd07..a2143f42aaf 100644 --- a/src/test/java/org/prebid/server/validation/ImpValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java @@ -1298,15 +1298,15 @@ public void validateImpsShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() } @Test - public void validateImpsShouldReturnValidationMessageWhenMetricValueIsNotValid() { + public void validateImpsShouldReturnValidationMessageWhenMetricValueNullOrEmpty() { // given final List givenImps = singletonList(validImpBuilder() - .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build()); + .metric(singletonList(Metric.builder().type("viewability").value(null).build())).build()); // when & then assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) .isInstanceOf(ValidationException.class) - .hasMessage("request.imp[0].metric[0].value must be in the range [0.0, 1.0]"); + .hasMessage("Missing request.imp[0].metric[0].value"); } @Test @@ -2370,15 +2370,15 @@ public void validateImpShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() } @Test - public void validateImpShouldReturnValidationMessageWhenMetricValueIsNotValid() { + public void validateImpShouldReturnValidationMessageWhenMetricValueNullOrEmpty() { // given final Imp givenImp = validImpBuilder() - .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build(); + .metric(singletonList(Metric.builder().type("viewability").value(null).build())).build(); // when & then assertThatThrownBy(() -> target.validateImp(givenImp)) .isInstanceOf(ValidationException.class) - .hasMessage("imp[id=200].metric[0].value must be in the range [0.0, 1.0]"); + .hasMessage("Missing imp[id=200].metric[0].value"); } private static List givenImps(UnaryOperator nativeCustomizer) { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-adswag-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-adswag-bid-request.json new file mode 100644 index 00000000000..152741f4075 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-adswag-bid-request.json @@ -0,0 +1,69 @@ +{ + "id": "test-request-banner", + "imp": [ + { + "id": "test-imp-banner", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "adswag": { + "placement_id": "prebid-test-display" + } + } + } + ], + "site": { + "id": "demo-site", + "domain": "example.com", + "page": "https://example.com/demo", + "publisher": { + "id": "prebid-test", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "dnt": 0, + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "ip": "192.0.2.1", + "language": "en" + }, + "user": { + "id": "demo-user" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "EUR" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "gdpr": 0 + }, + "ext": { + "prebid": { + "channel": { + "name": "web" + }, + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "http_method": "POST", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-adswag-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-adswag-bid-response.json new file mode 100644 index 00000000000..a78750b9c94 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-adswag-bid-response.json @@ -0,0 +1,25 @@ +{ + "id": "test-request-banner", + "cur": "EUR", + "seatbid": [ + { + "seat": "adswag", + "bid": [ + { + "id": "req-abc-1", + "impid": "test-imp-banner", + "price": 2.50, + "burl": "https://ads.adswag.ai/v1/win?sc=c2ln&sig=YWJj&price=${AUCTION_PRICE}", + "cid": "cmp-test-1", + "crid": "cr-test-1", + "adomain": ["advertiser.example"], + "ext": { + "adswag": { + "serve_url": "https://ads.adswag.ai/v1/ad?sc=c2ln&sig=YWJj" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-auction-adswag-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-auction-adswag-request.json new file mode 100644 index 00000000000..f4573456514 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-auction-adswag-request.json @@ -0,0 +1,45 @@ +{ + "id": "test-request-banner", + "imp": [ + { + "id": "test-imp-banner", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "adswag": { + "publisherId": "prebid-test", + "placementId": "prebid-test-display" + } + } + } + ], + "site": { + "id": "demo-site", + "domain": "example.com", + "page": "https://example.com/demo", + "publisher": { + "id": "demo-publisher" + } + }, + "device": { + "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "ip": "192.0.2.1", + "language": "en", + "dnt": 0 + }, + "user": { + "id": "demo-user" + }, + "cur": ["EUR"], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-auction-adswag-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-auction-adswag-response.json new file mode 100644 index 00000000000..4dac5a13406 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adswag/test-auction-adswag-response.json @@ -0,0 +1,48 @@ +{ + "id": "test-request-banner", + "seatbid": [ + { + "seat": "adswag", + "bid": [ + { + "id": "req-abc-1", + "impid": "test-imp-banner", + "price": 2.50, + "burl": "https://ads.adswag.ai/v1/win?sc=c2ln&sig=YWJj&price=${AUCTION_PRICE}", + "cid": "cmp-test-1", + "crid": "cr-test-1", + "adomain": ["advertiser.example"], + "adm": "", + "w": 300, + "h": 250, + "exp": 300, + "mtype": 1, + "ext": { + "adswag": { + "serve_url": "https://ads.adswag.ai/v1/ad?sc=c2ln&sig=YWJj" + }, + "origbidcpm": 2.50, + "origbidcur": "EUR", + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "adswag" + } + } + } + } + ], + "group": 0 + } + ], + "cur": "EUR", + "ext": { + "responsetimemillis": { + "adswag": "{{ adswag.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-appMonstaMedia-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-appMonstaMedia-bid-request.json new file mode 100644 index 00000000000..3f7a5e60358 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-appMonstaMedia-bid-request.json @@ -0,0 +1,57 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "bidder": { + "type": "publisher", + "placementId": "testPlacementId" + } + } + } + ], + "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", + "http_method": "POST", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-appMonstaMedia-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-appMonstaMedia-bid-response.json new file mode 100644 index 00000000000..180173549d8 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-appMonstaMedia-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/appMonstaMedia/test-auction-appMonstaMedia-request.json b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-auction-appMonstaMedia-request.json new file mode 100644 index 00000000000..dfc6fa5eb8b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-auction-appMonstaMedia-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "appMonstaMedia": { + "placementId": "testPlacementId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-auction-appMonstaMedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-auction-appMonstaMedia-response.json new file mode 100644 index 00000000000..a35eca173d9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/appMonstaMedia/test-auction-appMonstaMedia-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": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "appMonstaMedia" + } + } + } + } + ], + "seat": "appMonstaMedia", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "appMonstaMedia": "{{ appMonstaMedia.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-auction-bidwave-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-auction-bidwave-request.json new file mode 100644 index 00000000000..26df715cbd5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-auction-bidwave-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidwave": { + "publisherId": "11111111-1111-4111-8111-111111111111" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-auction-bidwave-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-auction-bidwave-response.json new file mode 100644 index 00000000000..59c661d9251 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-auction-bidwave-response.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "bidwave" + } + }, + "origbidcpm": 3.33, + "origbidcur": "USD" + } + } + ], + "seat": "bidwave", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "bidwave": "{{ bidwave.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-bidwave-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-bidwave-bid-request.json new file mode 100644 index 00000000000..31beccb6312 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-bidwave-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "publisherId": "11111111-1111-4111-8111-111111111111" + } + } + } + ], + "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", + "http_method": "POST", + "endpoint": "/openrtb2/auction" + } + }, + "bidwave": { + "pid": "11111111-1111-4111-8111-111111111111" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-bidwave-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-bidwave-bid-response.json new file mode 100644 index 00000000000..6df6c15a57d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidwave/test-bidwave-bid-response.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "mtype": 1, + "h": 250, + "w": 300 + } + ] + } + ], + "cur": "USD" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json index 66e6fdbeddc..bd9651297b2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json @@ -3,7 +3,7 @@ "imp": [ { "id": "imp_id", - "secure": 1, + "secure": 0, "banner": { "w": 300, "h": 250 @@ -11,14 +11,15 @@ "tagid": "15", "bidfloor": 14.7, "bidfloorcur": "USD", - "secure": 0, "ext": { "tid": "${json-unit.any-string}", "bidder": { "networkId": "12", "siteId": "15", "bidfloor": 14.7 - } + }, + "networkId": "12", + "siteId": "15" } } ], @@ -45,9 +46,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json new file mode 100644 index 00000000000..4ce042710fa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-request.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "floxis": { + "seat": "pub-seat-1" + } + } + } + ], + "source": { + "schain": { + "complete": 1, + "ver": "1.0", + "nodes": [ + { + "asi": "floxis.tech", + "sid": "pub-seat-1", + "hp": 1 + } + ] + } + }, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json new file mode 100644 index 00000000000..07b4c9cb073 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-auction-floxis-response.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "exp": 300, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "floxis" + } + }, + "origbidcpm": 3.33, + "origbidcur": "USD" + } + } + ], + "seat": "floxis", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "floxis": "{{ floxis.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json new file mode 100644 index 00000000000..cf7ba733859 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-request.json @@ -0,0 +1,66 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "seat": "pub-seat-1" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}", + "schain": { + "complete": 1, + "ver": "1.0", + "nodes": [ + { + "asi": "floxis.tech", + "sid": "pub-seat-1", + "hp": 1 + } + ] + } + }, + "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", + "http_method": "POST", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-response.json new file mode 100644 index 00000000000..c9c23670bfa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floxis/test-floxis-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "cur": "USD", + "seatbid": [ + { + "seat": "floxis", + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json index cacda5b188c..89c82b49442 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json @@ -11,7 +11,7 @@ "missena": { "apiKey": "apiKey", "placement": "placement", - "test": "test" + "sample": "sample" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json index 344c58cb6e6..4b0511cc829 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json @@ -10,6 +10,8 @@ "price": 10.2, "adm": "adm", "crid": "id", + "w": 320, + "h": 250, "ext": { "origbidcpm": 10.2, "origbidcur": "USD", 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 2feeee36d73..f5adf2e85c1 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 @@ -5,8 +5,9 @@ "request_id": "request_id", "timeout": "${json-unit.any-number}", "params": { + "apiKey": "apiKey", "placement": "placement", - "test": "test" + "sample": "sample" }, "ortb2": { "id": "request_id", @@ -23,7 +24,7 @@ "bidder": { "apiKey": "apiKey", "placement": "placement", - "test": "test" + "sample": "sample" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json index 4dece3dba9b..1c4c0cbfacd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json @@ -2,5 +2,7 @@ "requestId": "id", "cpm": 10.2, "ad": "adm", - "currency": "USD" + "currency": "USD", + "width": 320, + "height": 250 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-auction-sevio-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-auction-sevio-request.json new file mode 100644 index 00000000000..c42a183e75e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-auction-sevio-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "sevio": { + "placementId": "testPlacementId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-auction-sevio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-auction-sevio-response.json new file mode 100644 index 00000000000..32c3ee19b64 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-auction-sevio-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "crid": "crid1", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "sevio" + } + }, + "origbidcpm": 0.01, + "origbidcur": "USD" + } + } + ], + "seat": "sevio", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "sevio": "{{ sevio.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-sevio-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-sevio-bid-request.json new file mode 100644 index 00000000000..5a21bde73d9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-sevio-bid-request.json @@ -0,0 +1,57 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "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": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "http_method": "POST", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-sevio-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-sevio-bid-response.json new file mode 100644 index 00000000000..53d65871871 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/sevio/test-sevio-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "crid1", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "mtype": 1 + } + ], + "type": "banner" + } + ], + "cur": "USD" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json index e3fba9cb248..8f944980ed1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json @@ -17,8 +17,6 @@ ], "tmax": 5000, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json index 29e68d04d41..1fe5f17aa02 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json @@ -41,9 +41,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { 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 422675ce9f9..e8fc7ba04cd 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -82,6 +82,8 @@ adapters.adprime.enabled=true adapters.adprime.endpoint=http://localhost:8090/adprime-exchange adapters.adquery.enabled=true adapters.adquery.endpoint=http://localhost:8090/adquery-exchange +adapters.adswag.enabled=true +adapters.adswag.endpoint=http://localhost:8090/adswag-exchange adapters.adtarget.enabled=true adapters.adtarget.endpoint=http://localhost:8090/adtarget-exchange adapters.adtelligent.enabled=true @@ -170,6 +172,8 @@ adapters.bidstack.enabled=true adapters.bidstack.endpoint=http://localhost:8090/bidstack-exchange adapters.bidtheatre.enabled=true adapters.bidtheatre.endpoint=http://localhost:8090/bidtheatre-exchange +adapters.bidwave.enabled=true +adapters.bidwave.endpoint=http://localhost:8090/bidwave-exchange adapters.bigoad.enabled=true adapters.bigoad.endpoint=http://localhost:8090/bigoad-exchange adapters.blasto.enabled=true @@ -261,6 +265,8 @@ adapters.flatads.enabled=true adapters.flatads.endpoint=http://localhost:8090/flatads-exchange adapters.flipp.enabled=true adapters.flipp.endpoint=http://localhost:8090/flipp-exchange +adapters.floxis.enabled=true +adapters.floxis.endpoint=http://localhost:8090/floxis-exchange adapters.audiencenetwork.enabled=true adapters.audiencenetwork.endpoint=http://localhost:8090/audienceNetwork-exchange adapters.audiencenetwork.timeout-notification-url-template=http://localhost:8090/audienceNetwork-timeout @@ -515,6 +521,8 @@ adapters.seedingAlliance.aliases.finative.enabled=true adapters.seedingAlliance.aliases.finative.endpoint=http://localhost:8090/finative-exchange adapters.seedtag.enabled=true adapters.seedtag.endpoint=http://localhost:8090/seedtag-exchange +adapters.sevio.enabled=true +adapters.sevio.endpoint=http://localhost:8090/sevio-exchange adapters.smaato.enabled=true adapters.smaato.endpoint=http://localhost:8090/smaato-exchange adapters.smartadserver.enabled=true @@ -609,6 +617,8 @@ adapters.teqblaze.aliases.mycodemedia.enabled=true adapters.teqblaze.aliases.mycodemedia.endpoint=http://localhost:8090/mycodemedia-exchange/ adapters.teqblaze.aliases.adsmovil.enabled=true adapters.teqblaze.aliases.adsmovil.endpoint=http://localhost:8090/adsmovil-exchange/ +adapters.teqblaze.aliases.appMonstaMedia.enabled=true +adapters.teqblaze.aliases.appMonstaMedia.endpoint=http://localhost:8090/appMonstaMedia-exchange/ adapters.theadx.enabled=true adapters.theadx.endpoint=http://localhost:8090/theadx-exchange adapters.tradplus.enabled=true