diff --git a/docs/config-app.md b/docs/config-app.md index 52c454075a6..238a6511d37 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -346,6 +346,7 @@ For HTTP data source available next options: - `settings.http.amp-endpoint` - the url to fetch AMP stored requests. - `settings.http.video-endpoint` - the url to fetch video stored requests. - `settings.http.category-endpoint` - the url to fetch categories for long form video. +- `settings.http.rfc3986-compatible` - if equals to `true` the url will be build according to RFC 3986, `false` by default For account processing rules available next options: - `settings.enforce-valid-account` - if equals to `true` then request without account id will be rejected with 401. diff --git a/extra/pom.xml b/extra/pom.xml index b0e7b78c044..51a3974b9e6 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -39,6 +39,7 @@ 4.4 1.27.1 3.6.1 + 1.10.0 2.1 4.5.14 5.5.1 @@ -135,6 +136,11 @@ commons-math3 ${commons-math3.version} + + commons-validator + commons-validator + ${commons-validator.version} + org.apache.httpcomponents diff --git a/pom.xml b/pom.xml index b7573b1a282..f5df96e6d39 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,10 @@ org.apache.httpcomponents httpclient + + commons-validator + commons-validator + com.github.seancfoley ipaddress diff --git a/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java new file mode 100644 index 00000000000..5fdfe844c19 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java @@ -0,0 +1,186 @@ +package org.prebid.server.bidder.madsense; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +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; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.madsense.ExtImpMadsense; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; +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 MadsenseBidder implements Bidder { + + private static final String X_OPENRTB_VERSION_HEADER_VALUE = "2.6"; + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MadsenseBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final List videoImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + if (imp.getBanner() != null) { + try { + httpRequests.add(makeHttpRequest(request, Collections.singletonList(imp))); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } else if (imp.getVideo() != null) { + videoImps.add(imp); + } + } + + if (CollectionUtils.isNotEmpty(videoImps)) { + try { + httpRequests.add(makeHttpRequest(request, videoImps)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpMadsense parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext parameters"); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, List imps) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpMadsense extImp = parseImpExt(firstImp); + final String companyId = Objects.equals(request.getTest(), 1) ? "test" : extImp.getCompanyId(); + return BidderUtil.defaultRequest( + request.toBuilder().imp(imps).build(), + makeHeaders(request), + makeEndpoint(companyId), + mapper); + } + + private static MultiMap makeHeaders(BidRequest request) { + final MultiMap headers = HttpUtil.headers() + .set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION_HEADER_VALUE); + + final Device device = request.getDevice(); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + final Site site = request.getSite(); + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ORIGIN_HEADER, site.getDomain()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getRef()); + } + + return headers; + } + + private String makeEndpoint(String companyId) { + return endpointUrl + "?company_id=" + HttpUtil.encodeUrl(companyId); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + try { + final BidType bidType = getBidType(bid); + return BidderBid.builder() + .bid(bid) + .bidCurrency(currency) + .videoInfo(bidType == BidType.video + ? ExtBidPrebidVideo.of(resolveDuration(bid), resolveCategory(bid)) + : null) + .type(bidType) + .build(); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException( + "Unsupported bid mediaType: %s for impression: %s".formatted(bid.getMtype(), bid.getImpid())); + }; + } + + private static String resolveCategory(Bid bid) { + final List categories = bid.getCat(); + return CollectionUtils.isEmpty(categories) ? null : categories.getFirst(); + } + + private static Integer resolveDuration(Bid bid) { + final Integer duration = bid.getDur(); + return duration != null && duration > 0 ? duration : null; + } +} 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 e047a077599..d83274e4852 100644 --- a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -29,6 +29,7 @@ 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; @@ -44,6 +45,7 @@ public class TheTradeDeskBidder implements Bidder { private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$"); + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; private final String endpointUrl; private final String supplyId; @@ -180,32 +182,55 @@ private String resolveEndpoint(String sourceSupplyId) { public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse)); - } catch (DecodeException | PreBidException e) { + final List errors = new ArrayList<>(); + final List bids = extractBids(bidResponse, errors); + return Result.of(bids, errors); + } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidResponse bidResponse) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) - .map(SeatBid::getBid).filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) .toList(); } - private static BidType getBidType(Bid bid) { + private static BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null ? BidderBid.of(resolvePriceMacros(bid), bidType, currency) : null; + } + + private static BidType getBidType(Bid bid, List errors) { return switch (bid.getMtype()) { case 1 -> BidType.banner; case 2 -> BidType.video; case 4 -> BidType.xNative; - case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype())); + case null, default -> { + errors.add(BidderError.badServerResponse( + "could not define media type for impression: " + bid.getImpid())); + yield null; + } }; } + + private static Bid resolvePriceMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + return bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .build(); + } } diff --git a/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java b/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java index e7f91b12717..49e64645206 100644 --- a/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java +++ b/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java @@ -1,11 +1,14 @@ package org.prebid.server.bidder.visx; -import com.fasterxml.jackson.databind.JsonNode; +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.Device; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; +import io.vertx.core.MultiMap; 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; @@ -18,7 +21,10 @@ 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.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -26,6 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class VisxBidder implements Bidder { @@ -33,6 +40,9 @@ public class VisxBidder implements Bidder { private static final String DEFAULT_REQUEST_CURRENCY = "USD"; private static final Set SUPPORTED_BID_TYPES_TEXTUAL = Set.of("banner", "video"); + private static final TypeReference> BID_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + private final String endpointUrl; private final JacksonMapper mapper; @@ -43,20 +53,28 @@ public VisxBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - return Result.withValue(makeRequest(request)); - } - - private HttpRequest makeRequest(BidRequest bidRequest) { - final BidRequest outgoingRequest = modifyRequest(bidRequest); - return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + final BidRequest outgoingRequest = modifyRequest(request); + return Result.withValue( + BidderUtil.defaultRequest(outgoingRequest, makeHeaders(request.getDevice()), endpointUrl, mapper)); } - private BidRequest modifyRequest(BidRequest bidRequest) { + private static BidRequest modifyRequest(BidRequest bidRequest) { return CollectionUtils.isEmpty(bidRequest.getCur()) ? bidRequest.toBuilder().cur(Collections.singletonList(DEFAULT_REQUEST_CURRENCY)).build() : bidRequest; } + private static MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + return headers; + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { @@ -80,14 +98,14 @@ private List bidsFromResponse(BidRequest bidRequest, VisxResponse vis .map(VisxSeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(visxBid -> toBidderBid(bidRequest, visxBid)) + .map(visxBid -> toBidderBid(bidRequest, visxBid, visxResponse.getCur())) .toList(); } - private BidderBid toBidderBid(BidRequest bidRequest, VisxBid visxBid) { + private BidderBid toBidderBid(BidRequest bidRequest, VisxBid visxBid, String currency) { final Bid bid = toBid(visxBid, bidRequest.getId()); final BidType bidType = getBidType(bid.getExt(), bid.getImpid(), bidRequest.getImp()); - return BidderBid.of(bid, bidType, null); + return BidderBid.of(bid, bidType, StringUtils.defaultIfBlank(currency, null)); } private static Bid toBid(VisxBid visxBid, String id) { @@ -105,20 +123,24 @@ private static Bid toBid(VisxBid visxBid, String id) { .build(); } - private static BidType getBidType(ObjectNode bidExt, String impId, List imps) { + private BidType getBidType(ObjectNode bidExt, String impId, List imps) { final BidType extBidType = getBidTypeFromExt(bidExt); return extBidType != null ? extBidType : getBidTypeFromImp(impId, imps); } - private static BidType getBidTypeFromExt(ObjectNode bidExt) { - final JsonNode mediaTypeNode = bidExt != null ? bidExt.at("/prebid/meta/mediaType") : null; - final String bidTypeTextual = mediaTypeNode != null && mediaTypeNode.isTextual() - ? mediaTypeNode.asText() - : null; - - return bidTypeTextual != null && SUPPORTED_BID_TYPES_TEXTUAL.contains(bidTypeTextual) - ? BidType.valueOf(bidTypeTextual) - : null; + private BidType getBidTypeFromExt(ObjectNode bidExt) { + try { + return Optional.ofNullable(bidExt) + .map(ext -> mapper.mapper().convertValue(bidExt, BID_EXT_TYPE_REFERENCE)) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::getMediaType) + .filter(SUPPORTED_BID_TYPES_TEXTUAL::contains) + .map(BidType::valueOf) + .orElse(null); + } catch (IllegalArgumentException e) { + return null; + } } private static BidType getBidTypeFromImp(String impId, List imps) { diff --git a/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java b/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java index 0022d773875..df156830167 100644 --- a/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java +++ b/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java @@ -8,4 +8,6 @@ public class VisxResponse { List seatbid; + + String cur; } diff --git a/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java b/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java new file mode 100644 index 00000000000..85454c43f2d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java @@ -0,0 +1,141 @@ +package org.prebid.server.bidder.zeta_global_ssp; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp.ExtImpZetaGlobalSSP; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class ZetaGlobalSspBidder implements Bidder { + + private static final TypeReference> ZETA_GLOBAL_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final TypeReference> EXT_BID_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String SID_MACRO = "{{AccountID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ZetaGlobalSspBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(endpointUrl); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpZetaGlobalSSP extImp; + + try { + extImp = parseImpExt(firstImp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final HttpRequest httpRequest = BidderUtil.defaultRequest( + removeImpsExt(request), + resolveEndpoint(extImp), + mapper); + + return Result.withValues(Collections.singletonList(httpRequest)); + } + + private ExtImpZetaGlobalSSP parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ZETA_GLOBAL_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private String resolveEndpoint(ExtImpZetaGlobalSSP extImpZetaGlobalSSP) { + return endpointUrl + .replace(SID_MACRO, Objects.toString(extImpZetaGlobalSSP.getSid(), "0")); + } + + private BidRequest removeImpsExt(BidRequest request) { + final List imps = new ArrayList<>(request.getImp()); + final Imp firstImp = imps.getFirst().toBuilder().ext(null).build(); + imps.set(0, firstImp); + + return request.toBuilder() + .imp(imps) + .build(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || bidResponse.getSeatbid() == null) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType mediaType = getMediaType(bid, errors); + return mediaType == null ? null : BidderBid.of(bid, mediaType, currency); + } + + private BidType getMediaType(Bid bid, List errors) { + try { + return Optional.ofNullable(bid.getExt()) + .map(ext -> mapper.mapper().convertValue(ext, EXT_BID_TYPE_REFERENCE)) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(IllegalArgumentException::new); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badServerResponse( + "Failed to parse impression \"%s\" mediatype".formatted(bid.getImpid()))); + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java new file mode 100644 index 00000000000..6725007f682 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.madsense; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMadsense { + + String companyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java new file mode 100644 index 00000000000..b904d2e677a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpZetaGlobalSSP { + + Integer sid; +} diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 98517003baf..3dce1bcc12d 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -8,6 +8,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; @@ -25,6 +26,7 @@ import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -44,10 +46,14 @@ * In order to enable caching and reduce latency for read operations {@link HttpApplicationSettings} * can be decorated by {@link CachingApplicationSettings}. *

- * Expected the endpoint to satisfy the following API: + * Expected the endpoint to satisfy the following API (URL is encoded): *

* GET {endpoint}?request-ids=["req1","req2"]&imp-ids=["imp1","imp2","imp3"] *

+ * or settings.http.rfc3986-compatible is set to true + *

+ * * GET {endpoint}?request-id=req1&request-id=req2&imp-id=imp1&imp-id=imp2&imp-id=imp3 + * *

* This endpoint should return a payload like: *

  * {
@@ -76,20 +82,27 @@ public class HttpApplicationSettings implements ApplicationSettings {
     private final String categoryEndpoint;
     private final HttpClient httpClient;
     private final JacksonMapper mapper;
+    private final boolean isRfc3986Compatible;
+
+    public HttpApplicationSettings(HttpClient httpClient,
+                                   JacksonMapper mapper,
+                                   String endpoint,
+                                   String ampEndpoint,
+                                   String videoEndpoint,
+                                   String categoryEndpoint,
+                                   boolean isRfc3986Compatible) {
 
-    public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, String endpoint, String ampEndpoint,
-                                   String videoEndpoint, String categoryEndpoint) {
         this.httpClient = Objects.requireNonNull(httpClient);
         this.mapper = Objects.requireNonNull(mapper);
-        this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint));
-        this.ampEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(ampEndpoint));
-        this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint));
-        this.categoryEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(categoryEndpoint));
+        this.endpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(endpoint));
+        this.ampEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(ampEndpoint));
+        this.videoEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(videoEndpoint));
+        this.categoryEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(categoryEndpoint));
+        this.isRfc3986Compatible = isRfc3986Compatible;
     }
 
     @Override
     public Future getAccountById(String accountId, Timeout timeout) {
-
         return fetchAccountsByIds(Collections.singleton(accountId), timeout)
                 .map(accounts -> accounts.stream()
                         .findFirst()
@@ -111,15 +124,20 @@ private Future> fetchAccountsByIds(Set accountIds, Timeout
                 .recover(Future::failedFuture);
     }
 
-    private static String accountsRequestUrlFrom(String endpoint, Set accountIds) {
-        final StringBuilder url = new StringBuilder(endpoint);
-        url.append(endpoint.contains("?") ? "&" : "?");
-
-        if (!accountIds.isEmpty()) {
-            url.append("account-ids=[\"").append(joinIds(accountIds)).append("\"]");
+    private String accountsRequestUrlFrom(String endpoint, Set accountIds) {
+        try {
+            final URIBuilder uriBuilder = new URIBuilder(endpoint);
+            if (!accountIds.isEmpty()) {
+                if (isRfc3986Compatible) {
+                    accountIds.forEach(accountId -> uriBuilder.addParameter("account-id", accountId));
+                } else {
+                    uriBuilder.addParameter("account-ids", "[\"%s\"]".formatted(joinIds(accountIds)));
+                }
+            }
+            return uriBuilder.build().toString();
+        } catch (URISyntaxException e) {
+            throw new PreBidException("URL %s has bad syntax".formatted(endpoint));
         }
-
-        return url.toString();
     }
 
     private Future> processAccountsResponse(HttpClientResponse response, Set accountIds) {
@@ -165,9 +183,6 @@ public Future getAmpStoredData(String accountId, Set r
         return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout);
     }
 
-    /**
-     * Not supported and returns failed result.
-     */
     @Override
     public Future getVideoStoredData(String accountId, Set requestIds, Set impIds,
                                                        Timeout timeout) {
@@ -240,22 +255,27 @@ private Future fetchStoredData(String endpoint, Set re
                 .recover(exception -> failStoredDataResponse(exception, requestIds, impIds));
     }
 
-    private static String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) {
-        final StringBuilder url = new StringBuilder(endpoint);
-        url.append(endpoint.contains("?") ? "&" : "?");
-
-        if (!requestIds.isEmpty()) {
-            url.append("request-ids=[\"").append(joinIds(requestIds)).append("\"]");
-        }
-
-        if (!impIds.isEmpty()) {
+    private String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) {
+        try {
+            final URIBuilder uriBuilder = new URIBuilder(endpoint);
             if (!requestIds.isEmpty()) {
-                url.append("&");
+                if (isRfc3986Compatible) {
+                    requestIds.forEach(requestId -> uriBuilder.addParameter("request-id", requestId));
+                } else {
+                    uriBuilder.addParameter("request-ids", "[\"%s\"]".formatted(joinIds(requestIds)));
+                }
+            }
+            if (!impIds.isEmpty()) {
+                if (isRfc3986Compatible) {
+                    impIds.forEach(impId -> uriBuilder.addParameter("imp-id", impId));
+                } else {
+                    uriBuilder.addParameter("imp-ids", "[\"%s\"]".formatted(joinIds(impIds)));
+                }
             }
-            url.append("imp-ids=[\"").append(joinIds(impIds)).append("\"]");
+            return uriBuilder.build().toString();
+        } catch (URISyntaxException e) {
+            throw new PreBidException("URL %s has bad syntax".formatted(endpoint));
         }
-
-        return url.toString();
     }
 
     private static String joinIds(Set ids) {
diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
index 4e883ba2495..3f674ae814d 100644
--- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
@@ -122,10 +122,17 @@ HttpApplicationSettings httpApplicationSettings(
                 @Value("${settings.http.endpoint}") String endpoint,
                 @Value("${settings.http.amp-endpoint}") String ampEndpoint,
                 @Value("${settings.http.video-endpoint}") String videoEndpoint,
-                @Value("${settings.http.category-endpoint}") String categoryEndpoint) {
+                @Value("${settings.http.category-endpoint}") String categoryEndpoint,
+                @Value("${settings.http.rfc3986-compatible:false}") boolean isRfc3986Compatible) {
 
-            return new HttpApplicationSettings(httpClient, mapper, endpoint, ampEndpoint, videoEndpoint,
-                    categoryEndpoint);
+            return new HttpApplicationSettings(
+                    httpClient,
+                    mapper,
+                    endpoint,
+                    ampEndpoint,
+                    videoEndpoint,
+                    categoryEndpoint,
+                    isRfc3986Compatible);
         }
     }
 
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java
new file mode 100644
index 00000000000..a1cc34057e3
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java
@@ -0,0 +1,41 @@
+package org.prebid.server.spring.config.bidder;
+
+import org.prebid.server.bidder.BidderDeps;
+import org.prebid.server.bidder.madsense.MadsenseBidder;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
+import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
+import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
+import org.prebid.server.spring.env.YamlPropertySourceFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+import jakarta.validation.constraints.NotBlank;
+
+@Configuration
+@PropertySource(value = "classpath:/bidder-config/madsense.yaml", factory = YamlPropertySourceFactory.class)
+public class MadsenseConfiguration {
+
+    private static final String BIDDER_NAME = "madsense";
+
+    @Bean("madsenseConfigurationProperties")
+    @ConfigurationProperties("adapters.madsense")
+    BidderConfigurationProperties configurationProperties() {
+        return new BidderConfigurationProperties();
+    }
+
+    @Bean
+    BidderDeps madsenseBidderDeps(BidderConfigurationProperties madsenseConfigurationProperties,
+                                  @NotBlank @Value("${external-url}") String externalUrl,
+                                  JacksonMapper mapper) {
+
+        return BidderDepsAssembler.forBidder(BIDDER_NAME)
+                .withConfig(madsenseConfigurationProperties)
+                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
+                .bidderCreator(config -> new MadsenseBidder(config.getEndpoint(), mapper))
+                .assemble();
+    }
+}
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java
new file mode 100644
index 00000000000..aa98e645aa0
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java
@@ -0,0 +1,43 @@
+package org.prebid.server.spring.config.bidder;
+
+import org.prebid.server.bidder.BidderDeps;
+import org.prebid.server.bidder.zeta_global_ssp.ZetaGlobalSspBidder;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
+import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
+import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
+import org.prebid.server.spring.env.YamlPropertySourceFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+import jakarta.validation.constraints.NotBlank;
+
+@Configuration
+@PropertySource(value = "classpath:/bidder-config/zeta_global_ssp.yaml", factory = YamlPropertySourceFactory.class)
+public class ZetaGlobalSspConfiguration {
+
+    private static final String BIDDER_NAME = "zeta_global_ssp";
+
+    @Bean("zetaglobalsspConfigurationProperties")
+    @ConfigurationProperties("adapters.zetaglobalssp")
+    BidderConfigurationProperties configurationProperties() {
+        return new BidderConfigurationProperties();
+    }
+
+    @Bean
+    BidderDeps zetaGlobalSspBidderDeps(@Qualifier("zetaglobalsspConfigurationProperties")
+                                       BidderConfigurationProperties zetaGlobalSspConfigurationProperties,
+                                       @NotBlank @Value("${external-url}") String externalUrl,
+                                       JacksonMapper mapper) {
+
+        return BidderDepsAssembler.forBidder(BIDDER_NAME)
+                .withConfig(zetaGlobalSspConfigurationProperties)
+                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
+                .bidderCreator(config -> new ZetaGlobalSspBidder(config.getEndpoint(), mapper))
+                .assemble();
+    }
+}
diff --git a/src/main/java/org/prebid/server/util/HttpUtil.java b/src/main/java/org/prebid/server/util/HttpUtil.java
index ad9dd8a9238..47a5f24eda8 100644
--- a/src/main/java/org/prebid/server/util/HttpUtil.java
+++ b/src/main/java/org/prebid/server/util/HttpUtil.java
@@ -8,6 +8,7 @@
 import io.vertx.core.http.HttpServerResponse;
 import io.vertx.ext.web.RoutingContext;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.validator.routines.UrlValidator;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
@@ -78,12 +79,15 @@ public final class HttpUtil {
     public static final String MACROS_OPEN = "{{";
     public static final String MACROS_CLOSE = "}}";
 
+    private static final UrlValidator URL_VALIDAROR = UrlValidator.getInstance();
+
     private HttpUtil() {
     }
 
     /**
      * Checks the input string for using as URL.
      */
+    @Deprecated
     public static String validateUrl(String url) {
         if (containsMacrosses(url)) {
             return url;
@@ -96,6 +100,14 @@ public static String validateUrl(String url) {
         }
     }
 
+    public static String validateUrlSyntax(String url) {
+        if (containsMacrosses(url) || URL_VALIDAROR.isValid(url)) {
+            return url;
+        }
+
+        throw new IllegalArgumentException("URL supplied is not valid: " + url);
+    }
+
     // TODO: We need our own way to work with url macrosses
     private static boolean containsMacrosses(String url) {
         return StringUtils.contains(url, MACROS_OPEN) && StringUtils.contains(url, MACROS_CLOSE);
diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml
index 4aa01b82bbb..a6522be1122 100644
--- a/src/main/resources/bidder-config/generic.yaml
+++ b/src/main/resources/bidder-config/generic.yaml
@@ -41,27 +41,6 @@ adapters:
             - video
           supported-vendors:
           vendor-id: 0
-      zeta_global_ssp:
-        enabled: false
-        endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA
-        endpoint-compression: gzip
-        meta-info:
-          maintainer-email: DL-Zeta-SSP@zetaglobal.com
-          app-media-types:
-            - banner
-            - video
-          site-media-types:
-            - banner
-            - video
-          supported-vendors:
-          vendor-id: 833
-        usersync:
-          enabled: true
-          cookie-family-name: zeta_global_ssp
-          redirect:
-            url: https://ssp.disqus.com/redirectuser?sid=GET_SID_FROM_ZETA&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}}
-            uid-macro: 'BUYERUID'
-            support-cors: false
       blue:
         enabled: false
         endpoint: https://prebid-us-east-1.getblue.io/?src=prebid
diff --git a/src/main/resources/bidder-config/madsense.yaml b/src/main/resources/bidder-config/madsense.yaml
new file mode 100644
index 00000000000..29dc214b093
--- /dev/null
+++ b/src/main/resources/bidder-config/madsense.yaml
@@ -0,0 +1,13 @@
+adapters:
+  madsense:
+    endpoint: https://ads.madsense.io/pbs
+    meta-info:
+      maintainer-email: prebid@madsense.io
+      app-media-types:
+        - banner
+        - video
+      site-media-types:
+        - banner
+        - video
+      supported-vendors:
+      vendor-id: 0
diff --git a/src/main/resources/bidder-config/visx.yaml b/src/main/resources/bidder-config/visx.yaml
index 9d99f8a31f0..df8384fc6c1 100644
--- a/src/main/resources/bidder-config/visx.yaml
+++ b/src/main/resources/bidder-config/visx.yaml
@@ -1,6 +1,6 @@
 adapters:
   visx:
-    endpoint: https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_java
+    endpoint: https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard:0.1.2
     meta-info:
       maintainer-email: supply.partners@yoc.com
       app-media-types:
diff --git a/src/main/resources/bidder-config/zeta_global_ssp.yaml b/src/main/resources/bidder-config/zeta_global_ssp.yaml
new file mode 100644
index 00000000000..a1bd90b75f1
--- /dev/null
+++ b/src/main/resources/bidder-config/zeta_global_ssp.yaml
@@ -0,0 +1,23 @@
+adapters:
+  zeta_global_ssp:
+    endpoint: https://ssp.disqus.com/bid/prebid-server?sid={{AccountID}}
+    endpoint-compression: gzip
+    ortb-version: "2.6"
+    modifying-vast-xml-allowed: true
+    meta-info:
+      maintainer-email: DL-Zeta-SSP@zetaglobal.com
+      app-media-types:
+        - banner
+        - video
+        - audio
+      site-media-types:
+        - banner
+        - video
+        - audio
+      vendor-id: 833
+    usersync:
+      cookie-family-name: zeta_global_ssp
+      redirect:
+        url: https://ssp.disqus.com/redirectuser?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}}
+        support-cors: false
+        uid-macro: 'BUYERUID'
diff --git a/src/main/resources/static/bidder-params/madsense.json b/src/main/resources/static/bidder-params/madsense.json
new file mode 100644
index 00000000000..f45ac81f3ed
--- /dev/null
+++ b/src/main/resources/static/bidder-params/madsense.json
@@ -0,0 +1,16 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "madSense Adapter Params",
+  "description": "A schema which validates params accepted by the madSense adapter",
+  "type": "object",
+  "properties": {
+    "company_id": {
+      "type": "string",
+      "description": "An id used to identify madSense company",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "company_id"
+  ]
+}
diff --git a/src/main/resources/static/bidder-params/zeta_global_ssp.json b/src/main/resources/static/bidder-params/zeta_global_ssp.json
index 91ff05ed089..8a6d1d0a060 100644
--- a/src/main/resources/static/bidder-params/zeta_global_ssp.json
+++ b/src/main/resources/static/bidder-params/zeta_global_ssp.json
@@ -1,10 +1,13 @@
 {
   "$schema": "http://json-schema.org/draft-04/schema#",
   "title": "Zeta Global SSP Adapter Params",
-  "description": "A schema which validates params accepted by the Zeta SSP adapter",
-  "type": "object",
-
-  "properties": {},
+  "description": "A schema which validates params accepted by the Zeta Global SSP adapter",
 
-  "required": []
+  "type": "object",
+  "properties": {
+    "sid": {
+      "type": "integer",
+      "description": "An ID which identifies the publisher"
+    }
+  }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy
index 052bcf2f69f..41c781f372e 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy
@@ -129,8 +129,6 @@ LIMIT 1
          "adapters.generic.aliases.nativo.meta-info.site-media-types"         : "",
          "adapters.generic.aliases.infytv.meta-info.app-media-types"          : "",
          "adapters.generic.aliases.infytv.meta-info.site-media-types"         : "",
-         "adapters.generic.aliases.zeta-global-ssp.meta-info.app-media-types" : "",
-         "adapters.generic.aliases.zeta-global-ssp.meta-info.site-media-types": "",
          "adapters.generic.aliases.ccx.meta-info.app-media-types"             : "",
          "adapters.generic.aliases.ccx.meta-info.site-media-types"            : "",
          "adapters.generic.aliases.adrino.meta-info.app-media-types"          : "",
diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy
index 53faa7165fa..8022f2e8dcc 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/NetworkServiceContainer.groovy
@@ -8,6 +8,9 @@ class NetworkServiceContainer extends MockServerContainer {
 
     NetworkServiceContainer(String version) {
         super(DockerImageName.parse("mockserver/mockserver:mockserver-$version"))
+        def aliasWithTopLevelDomain = "${getNetworkAliases().first()}.com".toString()
+        withCreateContainerCmdModifier { it.withHostName(aliasWithTopLevelDomain) }
+        setNetworkAliases([aliasWithTopLevelDomain])
     }
 
     String getHostAndPort() {
diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy
index de271b4123a..5af648b2bc0 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy
@@ -1,13 +1,21 @@
 package org.prebid.server.functional.testcontainers.scaffolding
 
+import org.mockserver.matchers.Times
+import org.mockserver.model.Header
 import org.mockserver.model.HttpRequest
+import org.mockserver.model.HttpStatusCode
+import org.prebid.server.functional.model.ResponseModel
 import org.testcontainers.containers.MockServerContainer
 
 import static org.mockserver.model.HttpRequest.request
+import static org.mockserver.model.HttpResponse.response
+import static org.mockserver.model.HttpStatusCode.OK_200
+import static org.mockserver.model.MediaType.APPLICATION_JSON
 
 class HttpSettings extends NetworkScaffolding {
 
     private static final String ENDPOINT = "/stored-requests"
+    private static final String RFC_ENDPOINT = "/stored-requests-rfc"
     private static final String AMP_ENDPOINT = "/amp-stored-requests"
 
     HttpSettings(MockServerContainer mockServerContainer) {
@@ -27,12 +35,47 @@ class HttpSettings extends NetworkScaffolding {
 
     @Override
     void setResponse() {
+    }
+
+    protected HttpRequest getRfcRequest(String accountId) {
+        request().withPath(RFC_ENDPOINT)
+                .withQueryStringParameter("account-id", accountId)
+    }
+
+
+    void setRfcResponse(String value,
+                     ResponseModel responseModel,
+                     HttpStatusCode statusCode = OK_200,
+                     Map headers = [:]) {
+        def responseHeaders = headers.collect { new Header(it.key, it.value) }
+        def mockResponse = encode(responseModel)
+        mockServerClient.when(getRfcRequest(value), Times.unlimited())
+                .respond(response().withStatusCode(statusCode.code())
+                        .withBody(mockResponse, APPLICATION_JSON)
+                        .withHeaders(responseHeaders))
+    }
 
+    int getRfcRequestCount(String value) {
+        mockServerClient.retrieveRecordedRequests(getRfcRequest(value))
+                .size()
     }
 
     @Override
     void reset() {
         super.reset(ENDPOINT)
+        super.reset(RFC_ENDPOINT)
         super.reset(AMP_ENDPOINT)
     }
+
+    static String getEndpoint() {
+        return ENDPOINT
+    }
+
+    static String getAmpEndpoint() {
+        return AMP_ENDPOINT
+    }
+
+    static String getRfcEndpoint() {
+        return RFC_ENDPOINT
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy
index 2c6d1556a81..01f60ac2808 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy
@@ -15,7 +15,6 @@ import org.prebid.server.functional.testcontainers.PbsConfig
 import org.prebid.server.functional.testcontainers.scaffolding.HttpSettings
 import org.prebid.server.functional.util.PBSUtils
 import org.prebid.server.util.ResourceUtil
-import spock.lang.Shared
 
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
 import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer
@@ -23,11 +22,25 @@ import static org.prebid.server.functional.testcontainers.Dependencies.networkSe
 class HttpSettingsSpec extends BaseSpec {
 // Check that PBS actually applied account config only possible by relying on side effects.
 
-    @Shared
-    HttpSettings httpSettings = new HttpSettings(networkServiceContainer)
+    static PrebidServerService prebidServerService
+    static PrebidServerService prebidServerServiceWithRfc
 
-    @Shared
-    PrebidServerService prebidServerService = pbsServiceFactory.getService(PbsConfig.httpSettingsConfig)
+    private static final HttpSettings httpSettings = new HttpSettings(networkServiceContainer)
+    private static final Map PBS_CONFIG_WITH_RFC = new HashMap<>(PbsConfig.httpSettingsConfig) +
+            ['settings.http.endpoint': "${networkServiceContainer.rootUri}${HttpSettings.rfcEndpoint}".toString(),
+            'settings.http.rfc3986-compatible': 'true']
+
+    def setupSpec() {
+        prebidServerService = pbsServiceFactory.getService(PbsConfig.httpSettingsConfig)
+        prebidServerServiceWithRfc = pbsServiceFactory.getService(PBS_CONFIG_WITH_RFC)
+        bidder.setResponse()
+        vendorList.setResponse()
+    }
+
+    def cleanupSpec() {
+        prebidServerService = pbsServiceFactory.removeContainer(PbsConfig.httpSettingsConfig)
+        prebidServerService = pbsServiceFactory.removeContainer(PBS_CONFIG_WITH_RFC)
+    }
 
     def "PBS should take account information from http data source on auction request"() {
         given: "Get basic BidRequest with generic bidder and set gdpr = 1"
@@ -35,8 +48,8 @@ class HttpSettingsSpec extends BaseSpec {
         bidRequest.regs.gdpr = 1
 
         and: "Prepare default account response with gdpr = 0"
-        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest?.site?.publisher?.id)
-        httpSettings.setResponse(bidRequest?.site?.publisher?.id, httpSettingsResponse)
+        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest.accountId)
+        httpSettings.setResponse(bidRequest.accountId, httpSettingsResponse)
 
         when: "PBS processes auction request"
         def response = prebidServerService.sendAuctionRequest(bidRequest)
@@ -51,7 +64,32 @@ class HttpSettingsSpec extends BaseSpec {
         assert bidder.getRequestCount(bidRequest.id) == 1
 
         and: "There should be only one account request"
-        assert httpSettings.getRequestCount(bidRequest?.site?.publisher?.id) == 1
+        assert httpSettings.getRequestCount(bidRequest.accountId) == 1
+    }
+
+    def "PBS should take account information from http data source on auction request when rfc3986 enabled"() {
+        given: "Get basic BidRequest with generic bidder and set gdpr = 1"
+        def bidRequest = BidRequest.defaultBidRequest
+        bidRequest.regs.gdpr = 1
+
+        and: "Prepare default account response with gdpr = 0"
+        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest.accountId)
+        httpSettings.setRfcResponse(bidRequest.accountId, httpSettingsResponse)
+
+        when: "PBS processes auction request"
+        def response = prebidServerServiceWithRfc.sendAuctionRequest(bidRequest)
+
+        then: "Response should contain basic fields"
+        assert response.id
+        assert response.seatbid?.size() == 1
+        assert response.seatbid.first().seat == GENERIC
+        assert response.seatbid?.first()?.bid?.size() == 1
+
+        and: "There should be only one call to bidder"
+        assert bidder.getRequestCount(bidRequest.id) == 1
+
+        and: "There should be only one account request"
+        assert httpSettings.getRfcRequestCount(bidRequest.accountId) == 1
     }
 
     def "PBS should take account information from http data source on AMP request"() {
@@ -84,6 +122,36 @@ class HttpSettingsSpec extends BaseSpec {
         assert !response.ext?.debug?.httpcalls?.isEmpty()
     }
 
+    def "PBS should take account information from http data source on AMP request when rfc3986 enabled"() {
+        given: "Default AmpRequest"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Get basic stored request and set gdpr = 1"
+        def ampStoredRequest = BidRequest.defaultBidRequest
+        ampStoredRequest.site.publisher.id = ampRequest.account
+        ampStoredRequest.regs.gdpr = 1
+
+        and: "Save storedRequest into DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        and: "Prepare default account response with gdpr = 0"
+        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(ampRequest.account.toString())
+        httpSettings.setRfcResponse(ampRequest.account.toString(), httpSettingsResponse)
+
+        when: "PBS processes amp request"
+        def response = prebidServerServiceWithRfc.sendAmpRequest(ampRequest)
+
+        then: "Response should contain httpcalls"
+        assert !response.ext?.debug?.httpcalls?.isEmpty()
+
+        and: "There should be only one account request"
+        assert httpSettings.getRfcRequestCount(ampRequest.account.toString()) == 1
+
+        then: "Response should contain targeting"
+        assert !response.ext?.debug?.httpcalls?.isEmpty()
+    }
+
     def "PBS should take account information from http data source on event request"() {
         given: "Default EventRequest"
         def eventRequest = EventRequest.defaultEventRequest
@@ -103,6 +171,25 @@ class HttpSettingsSpec extends BaseSpec {
         assert httpSettings.getRequestCount(eventRequest.accountId.toString()) == 1
     }
 
+    def "PBS should take account information from http data source on event request when rfc3986 enabled"() {
+        given: "Default EventRequest"
+        def eventRequest = EventRequest.defaultEventRequest
+
+        and: "Prepare default account response"
+        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(eventRequest.accountId.toString())
+        httpSettings.setRfcResponse(eventRequest.accountId.toString(), httpSettingsResponse)
+
+        when: "PBS processes event request"
+        def responseBody = prebidServerServiceWithRfc.sendEventRequest(eventRequest)
+
+        then: "Event response should contain and corresponding content-type"
+        assert responseBody ==
+                ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png")
+
+        and: "There should be only one account request"
+        assert httpSettings.getRfcRequestCount(eventRequest.accountId.toString()) == 1
+    }
+
     def "PBS should take account information from http data source on setuid request"() {
         given: "Pbs config with adapters.generic.usersync.redirect.*"
         def pbsConfig = PbsConfig.httpSettingsConfig +
@@ -137,6 +224,42 @@ class HttpSettingsSpec extends BaseSpec {
         pbsServiceFactory.removeContainer(pbsConfig)
     }
 
+    def "PBS should take account information from http data source on setuid request when rfc3986 enabled"() {
+        given: "Pbs config with adapters.generic.usersync.redirect.*"
+        def pbsConfig = new HashMap<>(PbsConfig.httpSettingsConfig) +
+                ['settings.http.endpoint': "${networkServiceContainer.rootUri}${HttpSettings.rfcEndpoint}".toString(),
+                 'settings.http.rfc3986-compatible': 'true',
+                 'adapters.generic.usersync.redirect.url'            : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(),
+                 'adapters.generic.usersync.redirect.support-cors'   : 'false',
+                 'adapters.generic.usersync.redirect.format-override': 'blank']
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Get default SetuidRequest and set account, gdpr=1 "
+        def request = SetuidRequest.defaultSetuidRequest
+        request.gdpr = 1
+        request.account = PBSUtils.randomNumber.toString()
+        def uidsCookie = UidsCookie.defaultUidsCookie
+
+        and: "Prepare default account response"
+        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(request.account)
+        httpSettings.setRfcResponse(request.account, httpSettingsResponse)
+
+        when: "PBS processes setuid request"
+        def response = prebidServerService.sendSetUidRequest(request, uidsCookie)
+
+        then: "Response should contain tempUIDs cookie"
+        assert !response.uidsCookie.uids
+        assert response.uidsCookie.tempUIDs
+        assert response.responseBody ==
+                ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png")
+
+        and: "There should be only one account request"
+        assert httpSettings.getRfcRequestCount(request.account) == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
     def "PBS should take account information from http data source on vtrack request"() {
         given: "Default VtrackRequest"
         String payload = PBSUtils.randomString
@@ -162,6 +285,31 @@ class HttpSettingsSpec extends BaseSpec {
         assert prebidCacheRequest.contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}")
     }
 
+    def "PBS should take account information from http data source on vtrack request when rfc3986 enabled"() {
+        given: "Default VtrackRequest"
+        String payload = PBSUtils.randomString
+        def request = VtrackRequest.getDefaultVtrackRequest(encodeXml(Vast.getDefaultVastModel(payload)))
+        def accountId = PBSUtils.randomNumber.toString()
+
+        and: "Prepare default account response"
+        def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(accountId)
+        httpSettings.setRfcResponse(accountId, httpSettingsResponse)
+
+        when: "PBS processes vtrack request"
+        def response = prebidServerServiceWithRfc.sendVtrackRequest(request, accountId)
+
+        then: "Response should contain uid"
+        assert response.responses[0]?.uuid
+
+        and: "There should be only one account request and pbc request"
+        assert httpSettings.getRfcRequestCount(accountId.toString()) == 1
+        assert prebidCache.getXmlRequestCount(payload) == 1
+
+        and: "VastXml that was send to PrebidCache must contain event url"
+        def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload)[0]
+        assert prebidCacheRequest.contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}")
+    }
+
     def "PBS should return error if account settings isn't found"() {
         given: "Default EventRequest"
         def eventRequest = EventRequest.defaultEventRequest
@@ -174,4 +322,17 @@ class HttpSettingsSpec extends BaseSpec {
         assert exception.statusCode == 401
         assert exception.responseBody.contains("Account '$eventRequest.accountId' doesn't support events")
     }
+
+    def "PBS should return error if account settings isn't found when rfc3986 enabled"() {
+        given: "Default EventRequest"
+        def eventRequest = EventRequest.defaultEventRequest
+
+        when: "PBS processes event request"
+        prebidServerServiceWithRfc.sendEventRequest(eventRequest)
+
+        then: "Request should fail with error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == 401
+        assert exception.responseBody.contains("Account '$eventRequest.accountId' doesn't support events")
+    }
 }
diff --git a/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java b/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java
new file mode 100644
index 00000000000..ac4933dc439
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidder/madsense/MadsenseBidderTest.java
@@ -0,0 +1,368 @@
+package org.prebid.server.bidder.madsense;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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.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.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.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.madsense.ExtImpMadsense;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.prebid.server.bidder.model.BidderError.Type;
+import static org.prebid.server.bidder.model.BidderError.badInput;
+import static org.prebid.server.bidder.model.BidderError.badServerResponse;
+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.ORIGIN_HEADER;
+import static org.prebid.server.util.HttpUtil.REFERER_HEADER;
+import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER;
+import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER;
+import static org.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER;
+import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
+
+@ExtendWith(MockitoExtension.class)
+public class MadsenseBidderTest extends VertxTest {
+
+    private static final String ENDPOINT_URL = "https://ads.madsense.io/pbs";
+
+    private MadsenseBidder target;
+
+    @BeforeEach
+    public void setUp() {
+        target = new MadsenseBidder(ENDPOINT_URL, jacksonMapper);
+    }
+
+    @Test
+    public void creationShouldFailOnInvalidEndpointUrl() {
+        assertThatIllegalArgumentException().isThrownBy(() -> new MadsenseBidder("invalid_url", jacksonMapper));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorWhenFirstImpExtCouldNotBeParsed() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(imp -> imp
+                .banner(Banner.builder().build())
+                .ext(givenInvalidImpExt()));
+
+        // when
+        final Result>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).hasSize(1).containsExactly(badInput("Error parsing imp.ext parameters"));
+        assertThat(result.getValue()).isEmpty();
+    }
+
+    @Test
+    public void makeHttpRequestsShouldCreateSeparateRequestForEachBannerImp() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                builder -> builder.id("bannerImp1").banner(Banner.builder().build())
+                        .ext(givenImpExt("testCompanyId")),
+                builder -> builder.id("bannerImp2").banner(Banner.builder().build())
+                        .ext(givenImpExt("otherCompanyId")));
+
+        // when
+        final Result>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(2);
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getImpIds)
+                .containsExactlyInAnyOrder(Set.of("bannerImp1"), Set.of("bannerImp2"));
+
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getUri)
+                .containsOnly(ENDPOINT_URL + "?company_id=testCompanyId");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldCreateOneRequestForAllVideoImps() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                builder -> builder.id("videoImp1").video(Video.builder().build())
+                        .ext(givenImpExt("testCompanyId")),
+                builder -> builder.id("videoImp2").video(Video.builder().build())
+                        .ext(givenImpExt("otherCompanyId")));
+
+        // when
+        final Result>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1);
+        assertThat(result.getValue().getFirst().getImpIds()).containsExactlyInAnyOrder("videoImp1", "videoImp2");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldHandleMixedBannerAndVideoImps() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                builder -> builder.id("bannerImp1").banner(Banner.builder().build())
+                        .ext(givenImpExt("testCompanyId")),
+                builder -> builder.id("videoImp1").video(Video.builder().build())
+                        .ext(givenImpExt("otherCompanyId1")),
+                builder -> builder.id("bannerImp2").banner(Banner.builder().build())
+                        .ext(givenImpExt("otherCompanyId2")),
+                builder -> builder.id("videoImp2").video(Video.builder().build())
+                        .ext(givenImpExt("otherCompanyId3")));
+
+        // when
+        final Result>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(3)
+                .extracting(HttpRequest::getImpIds)
+                .containsExactly(Set.of("bannerImp1"), Set.of("bannerImp2"), Set.of("videoImp1", "videoImp2"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldUseTestCompanyIdWhenRequestTestIsOne() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                builder -> builder.banner(Banner.builder().build()).ext(givenImpExt("testCompanyId")))
+                .toBuilder()
+                .test(1)
+                .build();
+
+        // when
+        final Result>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getUri)
+                .containsExactly(ENDPOINT_URL + "?company_id=test");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetCorrectHeaders() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.banner(Banner.builder().build()))
+                .toBuilder()
+                .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build())
+                .site(Site.builder().domain("domain").ref("referrer").build())
+                .build();
+
+        // 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);
+                    assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE);
+                    assertThat(headers.get(X_OPENRTB_VERSION_HEADER)).isEqualTo("2.6");
+                    assertThat(headers.get(USER_AGENT_HEADER)).isEqualTo("ua");
+                    assertThat(headers.get(X_FORWARDED_FOR_HEADER)).isEqualTo("ip");
+                    assertThat(headers.get(ORIGIN_HEADER)).isEqualTo("domain");
+                    assertThat(headers.get(REFERER_HEADER)).isEqualTo("referrer");
+                });
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetIpv6WhenIpIsMissing() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.banner(Banner.builder().build()))
+                .toBuilder()
+                .device(Device.builder().ipv6("ipv6").ip(null).build())
+                .build();
+
+        // when
+        final Result>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue().getFirst().getHeaders().get(X_FORWARDED_FOR_HEADER))
+                .isEqualTo("ipv6");
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
+        // given
+        final BidderCall httpCall = givenHttpCall("invalid_json");
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).hasSize(1)
+                .allSatisfy(error -> {
+                    assertThat(error.getType()).isEqualTo(Type.bad_server_response);
+                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'");
+                });
+        assertThat(result.getValue()).isEmpty();
+    }
+
+    @Test
+    public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException {
+        // given
+        final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null));
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).isEmpty();
+    }
+
+    @Test
+    public void makeBidsShouldReturnEmptyListWhenSeatBidIsEmpty() throws JsonProcessingException {
+        // given
+        final BidResponse bidResponseEmptySeatBid = BidResponse.builder().seatbid(emptyList()).build();
+        final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponseEmptySeatBid));
+
+        // when
+        final Result> resultEmptySeatBid = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(resultEmptySeatBid.getErrors()).isEmpty();
+        assertThat(resultEmptySeatBid.getValue()).isEmpty();
+    }
+
+    @Test
+    public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException {
+        // given
+        final Bid bannerBid = Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE)
+                .adm("adm1").mtype(1).cat(singletonList("cat1")).build();
+        final BidResponse bidResponse = BidResponse.builder()
+                .cur("USD")
+                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bannerBid)).build()))
+                .build();
+        final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse));
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnVideoBidSuccessfullyWithVideoInfo() throws JsonProcessingException {
+        // given
+        final Bid videoBid = Bid.builder().id("bidId1").mtype(2).cat(singletonList("cat-video")).dur(30).build();
+        final BidResponse bidResponse = BidResponse.builder()
+                .cur("EUR")
+                .seatbid(singletonList(SeatBid.builder().bid(singletonList(videoBid)).build()))
+                .build();
+        final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse));
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        final BidderBid expectedBidderBid = BidderBid.builder()
+                .bid(videoBid)
+                .type(BidType.video)
+                .bidCurrency("EUR")
+                .videoInfo(ExtBidPrebidVideo.of(30, "cat-video"))
+                .build();
+
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1).containsExactly(expectedBidderBid);
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorWhenBidMtypeIsMissing() throws JsonProcessingException {
+        // given
+        final Bid bid = Bid.builder().id("bidId1").impid("impId1").build();
+        final BidResponse bidResponse = BidResponse.builder()
+                .cur("USD")
+                .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid)).build()))
+                .build();
+        final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse));
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getValue()).isEmpty();
+        assertThat(result.getErrors()).hasSize(1)
+                .containsExactly(badServerResponse("Unsupported bid mediaType: null for impression: impId1"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorWhenBidMtypeIsUnsupported() throws JsonProcessingException {
+        // given
+        final Bid bid = Bid.builder().id("bidId1").impid("impId1").mtype(3).build();
+        final BidResponse bidResponse = BidResponse.builder()
+                .cur("USD")
+                .seatbid(singletonList(SeatBid.builder()
+                        .bid(singletonList(bid)).build()))
+                .build();
+        final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse));
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getValue()).isEmpty();
+        assertThat(result.getErrors()).hasSize(1)
+                .containsExactly(badServerResponse("Unsupported bid mediaType: 3 for impression: impId1"));
+    }
+
+    private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) {
+        final List imps = Arrays.stream(impCustomizers)
+                .map(MadsenseBidderTest::givenImp)
+                .toList();
+        return BidRequest.builder().imp(imps).cur(singletonList("USD")).build();
+    }
+
+    private static Imp givenImp(UnaryOperator impCustomizer) {
+        return impCustomizer.apply(Imp.builder()
+                        .id("test-imp-id")
+                        .ext(givenImpExt("testCompanyId")))
+                .build();
+    }
+
+    private static ObjectNode givenImpExt(String companyId) {
+        return mapper.valueToTree(ExtPrebid.of(null, ExtImpMadsense.of(companyId)));
+    }
+
+    private static ObjectNode givenInvalidImpExt() {
+        return mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()));
+    }
+
+    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 a292ac6d385..8c167590e4f 100644
--- a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java
@@ -24,6 +24,7 @@
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk;
 
+import java.math.BigDecimal;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -33,6 +34,7 @@
 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;
 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;
@@ -458,7 +460,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
     }
 
     @Test
-    public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
+    public void makeBidsShouldReturnErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
         // given
         final BidderCall httpCall = givenHttpCall(
                 givenBidResponse(bidBuilder -> bidBuilder.impid("123")));
@@ -469,7 +471,234 @@ public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessi
         // then
         assertThat(result.getValue()).isEmpty();
         assertThat(result.getErrors()).hasSize(1)
-                .containsOnly(BidderError.badServerResponse("unsupported mtype: null"));
+                .containsOnly(BidderError.badServerResponse("could not define media type for impression: 123"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnValidBidsAndErrorsForMixedMediaTypes() throws JsonProcessingException {
+        // given
+        final BidderCall httpCall = givenHttpCall(
+                mapper.writeValueAsString(BidResponse.builder()
+                        .cur("USD")
+                        .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(2).impid("valid2").build(),  // valid video
+                                        Bid.builder().mtype(null).impid("invalid2").build() // null mtype
+                                ))
+                                .build()))
+                        .build()));
+
+        // when
+        final Result> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getValue()).hasSize(2)
+                .extracting(bidderBid -> bidderBid.getBid().getImpid())
+                .containsExactly("valid1", "valid2");
+        assertThat(result.getErrors()).hasSize(2)
+                .containsExactly(
+                        BidderError.badServerResponse("could not define media type for impression: invalid1"),
+                        BidderError.badServerResponse("could not define media type for impression: invalid2"));
+    }
+
+    @Test
+    public void makeBidsShouldReplacePriceMacroInNurlAndAdmWithBidPrice() throws JsonProcessingException {
+        // given
+        final BidderCall httpCall = givenHttpCall(
+                givenBidResponse(bidBuilder -> bidBuilder
+                        .mtype(1)
+                        .impid("123")
+                        .price(BigDecimal.valueOf(1.23))
+                        .nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
+                        .adm("
Price: ${AUCTION_PRICE}
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm, Bid::getPrice) + .containsOnly(tuple("http://example.com/nurl?price=1.23", "
Price: 1.23
", BigDecimal.valueOf(1.23))); + } + + @Test + public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(null) + .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") + .adm("
Price: ${AUCTION_PRICE}
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl?price=0", "
Price: 0
")); + } + + @Test + public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsZero() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.ZERO) + .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") + .adm("
Price: ${AUCTION_PRICE}
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl?price=0", "
Price: 0
")); + } + + @Test + public void makeBidsShouldReplacePriceMacroInNurlOnlyWhenAdmDoesNotContainMacro() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.valueOf(5.67)) + .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") + .adm("
No macro here
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl?price=5.67", "
No macro here
")); + } + + @Test + public void makeBidsShouldReplacePriceMacroInAdmOnlyWhenNurlDoesNotContainMacro() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.valueOf(8.90)) + .nurl("http://example.com/nurl") + .adm("
Price: ${AUCTION_PRICE}
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl", "
Price: 8.9
")); + } + + @Test + public void makeBidsShouldNotReplacePriceMacroWhenNurlAndAdmDoNotContainMacro() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.valueOf(12.34)) + .nurl("http://example.com/nurl") + .adm("
No macro
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl", "
No macro
")); + } + + @Test + public void makeBidsShouldHandleNullNurlAndAdm() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.valueOf(15.00)) + .nurl(null) + .adm(null))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple(null, null)); + } + + @Test + public void makeBidsShouldReplaceMultiplePriceMacrosInSameField() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(BigDecimal.valueOf(9.99)) + .nurl("http://example.com/nurl?price=${AUCTION_PRICE}&backup_price=${AUCTION_PRICE}") + .adm("
Price: ${AUCTION_PRICE}, Fallback: ${AUCTION_PRICE}
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl?price=9.99&backup_price=9.99", "
Price: 9.99, Fallback: 9.99
")); + } + + @Test + public void makeBidsShouldHandleLargeDecimalPrices() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder + .mtype(1) + .impid("123") + .price(new BigDecimal("123456789.123456789")) + .nurl("http://example.com/nurl?price=${AUCTION_PRICE}") + .adm("
Price: ${AUCTION_PRICE}
"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsOnly(tuple("http://example.com/nurl?price=123456789.123456789", "
Price: 123456789.123456789
")); } private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { @@ -508,5 +737,4 @@ private static ObjectNode impExt(String publisherId) { private static ObjectNode impExt(String publisherId, String supplySourceId) { return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId, supplySourceId))); } - } diff --git a/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java b/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java index cd11937bd94..d3640d344b7 100644 --- a/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/visx/VisxBidderTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; @@ -22,16 +23,19 @@ import org.prebid.server.bidder.visx.model.VisxSeatBid; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.visx.ExtImpVisx; +import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.Map; 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.assertj.core.api.Assertions.tuple; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -68,6 +72,48 @@ public void makeHttpRequestsShouldNotModifyIncomingRequest() { .containsExactly(bidRequest); } + @Test + public void makeHttpRequestsShouldAddIp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpVisx.of(123, Arrays.asList(10, 20))))) + .build())) + .device(Device.builder().ip("someIp").ipv6("ipv6").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .flatExtracting(res -> res.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .contains(tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "someIp")); + } + + @Test + public void makeHttpRequestsShouldAddIpv6IfIpIsNotPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpVisx.of(123, Arrays.asList(10, 20))))) + .build())) + .device(Device.builder().ipv6("ipv6").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .flatExtracting(res -> res.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .contains(tuple(HttpUtil.X_FORWARDED_FOR_HEADER.toString(), "ipv6")); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given @@ -122,7 +168,7 @@ public void makeBidsShouldReturnBidWithTypeBannerIfBannerIsPresent() throws Json // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).containsExactly( - BidderBid.of(Bid.builder().id("id").impid("123").build(), banner, null)); + BidderBid.of(Bid.builder().id("id").impid("123").build(), banner, "USD")); } @Test @@ -138,7 +184,7 @@ public void makeBidsShouldReturnBidWithTypeBannerIfVideoIsPresentAndBannerIsAbse // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).containsExactly( - BidderBid.of(Bid.builder().id("id").impid("123").build(), video, null)); + BidderBid.of(Bid.builder().id("id").impid("123").build(), video, "USD")); } @Test @@ -194,7 +240,7 @@ public void makeBidsShouldFavourBidExtMediaTypeToImpMediaTypeWhenPresent() throw // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(givenBid(identity()), banner, null)); + .containsExactly(BidderBid.of(givenBid(identity()), banner, "USD")); } @Test @@ -213,7 +259,7 @@ public void makeBidsShouldReturnImpMediaTypeWhenBidExtMediaTypeIsAbsent() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).containsExactly( - BidderBid.of(givenBid(bidBuilder -> bidBuilder.ext(null)), video, null)); + BidderBid.of(givenBid(bidBuilder -> bidBuilder.ext(null)), video, "USD")); } @Test @@ -235,7 +281,7 @@ public void makeBidsShouldReturnImpMediaTypeWhenBidExtMediaTypeIsInvalid() throw // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).containsExactly( - BidderBid.of(givenBid(bidBuilder -> bidBuilder.ext(givenBidExt("123"))), video, null)); + BidderBid.of(givenBid(bidBuilder -> bidBuilder.ext(givenBidExt("123"))), video, "USD")); } @Test @@ -289,7 +335,8 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .h(100) .adomain(singletonList("adomain")) .build(), - video, null); + video, + "USD"); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).containsExactly(expected); @@ -314,7 +361,7 @@ private static Imp givenImp(UnaryOperator impCustomizer) { private static VisxResponse givenVisxResponse(UnaryOperator bidCustomizer, String seat) { return VisxResponse.of(singletonList(VisxSeatBid.of( - singletonList(bidCustomizer.apply(VisxBid.builder()).build()), seat))); + singletonList(bidCustomizer.apply(VisxBid.builder()).build()), seat)), "USD"); } private static Bid givenBid(UnaryOperator bidCustomizer) { diff --git a/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java b/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java new file mode 100644 index 00000000000..886af999d74 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java @@ -0,0 +1,221 @@ +package org.prebid.server.bidder.zeta_global_ssp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.theadx.TheadxBidder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp.ExtImpZetaGlobalSSP; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.Type.bad_server_response; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class ZetaGlobalSspBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com/{{AccountID}}"; + + private final ZetaGlobalSspBidder target = new ZetaGlobalSspBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void shouldFailOnBidderCreation() { + assertThatIllegalArgumentException().isThrownBy(() -> new TheadxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtMissing() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp(imp -> imp.id("imp1") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).contains("Missing bidder ext in impression with id: imp1"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateSingleRequestAndRemoveImpExt() { + // given + final Imp imp1 = givenImp(imp -> imp.id("imp1").ext(givenImpExt(11))); + final Imp imp2 = givenImp(imp -> imp.id("imp2").ext(givenImpExt(44))); + final BidRequest bidRequest = givenBidRequest(imp1, imp2); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + final HttpRequest httpRequest = result.getValue().getFirst(); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(null, givenImpExt(44)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> { + assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); + assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); + }); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldResolveMacroInEndpointUrl() { + // given + final Imp imp1 = givenImp(imp -> imp.id("imp1").ext(givenImpExt(11))); + final BidRequest bidRequest = givenBidRequest(imp1); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test-url.com/11"); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyInvalid() { + // given + final BidderCall httpCall = givenHttpCall("invalid-response-body"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).contains("Failed to decode:"); + assertThat(error.getType()).isEqualTo(bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfSeatBidIsNullOrEmpty() throws JsonProcessingException { + // given + final BidderCall httpCall = + givenHttpCall(mapper.writeValueAsString(BidResponse.builder().cur("USD").build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfCannotResolveBidType() throws JsonProcessingException { + // given + final Bid bid = givenBid("imp1", mapper.createObjectNode()); + final BidderCall httpCall = + givenHttpCall(mapper.writeValueAsString(givenBidResponse(List.of(bid)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).contains("Failed to parse impression \"imp1\" mediatype"); + assertThat(error.getType()).isEqualTo(bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnBannerBidIfTypeParsedProperly() throws JsonProcessingException { + // given + final ObjectNode extWithPrebidType = mapper.createObjectNode(); + extWithPrebidType.putObject("prebid").put("type", "banner"); + final Bid validBid = givenBid("imp1", extWithPrebidType); + + final BidResponse bidResponse = givenBidResponse(List.of(validBid)); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final BidderBid bidderBid = result.getValue().getFirst(); + assertThat(bidderBid.getBid().getImpid()).isEqualTo("imp1"); + assertThat(bidderBid.getType()).isEqualTo(BidType.banner); + assertThat(bidderBid.getBidCurrency()).isEqualTo("USD"); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(List.of(imps)).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("imp_id").ext(givenImpExt(11))).build(); + } + + private static ObjectNode givenImpExt(Integer sid) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpZetaGlobalSSP.of(sid))); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private static Bid givenBid(String impId, ObjectNode ext) { + return Bid.builder().impid(impId).ext(ext).build(); + } + + private static BidResponse givenBidResponse(List bids) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(bids).build())) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/it/MadsenseTest.java b/src/test/java/org/prebid/server/it/MadsenseTest.java new file mode 100644 index 00000000000..fd7366fafb6 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MadsenseTest.java @@ -0,0 +1,33 @@ +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 MadsenseTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMadsense() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/madsense-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/madsense/test-madsense-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/madsense/test-madsense-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/madsense/test-auction-madsense-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/madsense/test-auction-madsense-response.json", response, + singletonList("madsense")); + } +} diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index e3076ddbdfd..60627cf2571 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.vertx.core.Future; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URIBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,12 +24,14 @@ import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; +import java.net.URISyntaxException; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; import java.util.Collections; import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeoutException; import static java.util.Arrays.asList; @@ -49,10 +53,10 @@ @ExtendWith(MockitoExtension.class) public class HttpApplicationSettingsTest extends VertxTest { - private static final String ENDPOINT = "http://stored-requests"; - private static final String AMP_ENDPOINT = "http://amp-stored-requests"; - private static final String VIDEO_ENDPOINT = "http://video-stored-requests"; - private static final String CATEGORY_ENDPOINT = "http://category-requests"; + private static final String ENDPOINT = "http://stored-requests.com/something?id=1"; + private static final String AMP_ENDPOINT = "http://amp-stored-requests.com/something?id=2"; + private static final String VIDEO_ENDPOINT = "http://video-stored-requests.com/something?id=3"; + private static final String CATEGORY_ENDPOINT = "http://category-requests.com/something"; @Mock(strictness = LENIENT) private HttpClient httpClient; @@ -65,7 +69,7 @@ public class HttpApplicationSettingsTest extends VertxTest { @BeforeEach public void setUp() { httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, AMP_ENDPOINT, - VIDEO_ENDPOINT, CATEGORY_ENDPOINT); + VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false); final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); final TimeoutFactory timeoutFactory = new TimeoutFactory(clock); @@ -77,7 +81,7 @@ public void setUp() { public void creationShouldFailsOnInvalidEndpoint() { assertThatIllegalArgumentException() .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, "invalid_url", AMP_ENDPOINT, - VIDEO_ENDPOINT, CATEGORY_ENDPOINT)) + VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false)) .withMessage("URL supplied is not valid: invalid_url"); } @@ -85,7 +89,7 @@ public void creationShouldFailsOnInvalidEndpoint() { public void creationShouldFailsOnInvalidAmpEndpoint() { assertThatIllegalArgumentException() .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, "invalid_url", - VIDEO_ENDPOINT, CATEGORY_ENDPOINT)) + VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false)) .withMessage("URL supplied is not valid: invalid_url"); } @@ -93,7 +97,7 @@ public void creationShouldFailsOnInvalidAmpEndpoint() { public void creationShouldFailsOnInvalidVideoEndpoint() { assertThatIllegalArgumentException() .isThrownBy(() -> new HttpApplicationSettings(httpClient, jacksonMapper, ENDPOINT, AMP_ENDPOINT, - "invalid_url", CATEGORY_ENDPOINT)) + "invalid_url", CATEGORY_ENDPOINT, false)) .withMessage("URL supplied is not valid: invalid_url"); } @@ -118,7 +122,40 @@ public void getAccountByIdShouldReturnFetchedAccount() throws JsonProcessingExce assertThat(future.result().getId()).isEqualTo("someId"); assertThat(future.result().getAuction().getPriceGranularity()).isEqualTo("testPriceGranularity"); - verify(httpClient).get(eq("http://stored-requests?account-ids=[\"someId\"]"), any(), + verify(httpClient).get( + eq("http://stored-requests.com/something?id=1&account-ids=%5B%22someId%22%5D"), + any(), + anyLong()); + } + + @Test + public void getAccountByIdShouldReturnFetchedAccountWithRfc3986CompatibleParams() throws JsonProcessingException { + // given + givenHttpClientReturnsResponse(200, null); + httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, + ENDPOINT, AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT, true); + + final Account account = Account.builder() + .id("someId") + .auction(AccountAuctionConfig.builder() + .priceGranularity("testPriceGranularity") + .build()) + .privacy(AccountPrivacyConfig.builder().build()) + .build(); + final HttpAccountsResponse response = HttpAccountsResponse.of(Collections.singletonMap("someId", account)); + givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response)); + + // when + final Future future = httpApplicationSettings.getAccountById("someId", timeout); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result().getId()).isEqualTo("someId"); + assertThat(future.result().getAuction().getPriceGranularity()).isEqualTo("testPriceGranularity"); + + verify(httpClient).get( + eq("http://stored-requests.com/something?id=1&account-id=someId"), + any(), anyLong()); } @@ -234,8 +271,11 @@ public void getStoredDataShouldSendHttpRequestWithExpectedNewParams() { new HashSet<>(asList("id3", "id4")), timeout); // then - verify(httpClient).get(eq("http://stored-requests?request-ids=[\"id2\",\"id1\"]&imp-ids=[\"id4\",\"id3\"]"), - any(), anyLong()); + verify(httpClient).get( + eq("http://stored-requests.com/something" + + "?id=1&request-ids=%5B%22id2%22%2C%22id1%22%5D&imp-ids=%5B%22id4%22%2C%22id3%22%5D"), + any(), + anyLong()); } @Test @@ -243,16 +283,45 @@ public void getStoredDataShouldSendHttpRequestWithExpectedAppendedParams() { // given givenHttpClientReturnsResponse(200, null); httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, - "http://some-domain?param1=value1", AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT); + "http://some-domain.com?param1=value1", AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT, false); // when httpApplicationSettings.getStoredData(null, singleton("id1"), singleton("id2"), timeout); // then - verify(httpClient).get(eq("http://some-domain?param1=value1&request-ids=[\"id1\"]&imp-ids=[\"id2\"]"), any(), + verify(httpClient).get( + eq("http://some-domain.com?param1=value1&request-ids=%5B%22id1%22%5D&imp-ids=%5B%22id2%22%5D"), + any(), anyLong()); } + @Test + public void getStoredDataShouldSendHttpRequestWithRfc3986CompatibleParams() throws URISyntaxException { + // given + givenHttpClientReturnsResponse(200, null); + httpApplicationSettings = new HttpApplicationSettings(httpClient, jacksonMapper, + ENDPOINT, AMP_ENDPOINT, VIDEO_ENDPOINT, CATEGORY_ENDPOINT, true); + + // when + httpApplicationSettings.getStoredData(null, Set.of("id1", "id2"), Set.of("id1", "id2"), timeout); + + // then + final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(httpClient).get(captor.capture(), any(), anyLong()); + + final URIBuilder uriBuilder = new URIBuilder(captor.getValue()); + assertThat(uriBuilder.getHost()).isEqualTo("stored-requests.com"); + assertThat(uriBuilder.getPath()).isEqualTo("/something"); + assertThat(uriBuilder.getQueryParams()) + .extracting(NameValuePair::getName, NameValuePair::getValue) + .containsExactlyInAnyOrder( + tuple("id", "1"), + tuple("request-id", "id1"), + tuple("request-id", "id2"), + tuple("imp-id", "id1"), + tuple("imp-id", "id2")); + } + @Test public void getStoredDataShouldReturnResultWithErrorIfHttpClientFails() { // given @@ -416,7 +485,7 @@ public void getCategoriesShouldBuildUrlFromEndpointAdServerAndPublisher() { // then final ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(String.class); verify(httpClient).get(urlArgumentCaptor.capture(), anyLong()); - assertThat(urlArgumentCaptor.getValue()).isEqualTo("http://category-requests/primaryAdServer/publisher.json"); + assertThat(urlArgumentCaptor.getValue()).isEqualTo("http://category-requests.com/something/primaryAdServer/publisher.json"); } @Test @@ -431,7 +500,8 @@ public void getCategoriesShouldBuildUrlFromEndpointAdServer() { // then final ArgumentCaptor urlArgumentCaptor = ArgumentCaptor.forClass(String.class); verify(httpClient).get(urlArgumentCaptor.capture(), anyLong()); - assertThat(urlArgumentCaptor.getValue()).isEqualTo("http://category-requests/primaryAdServer.json"); + assertThat(urlArgumentCaptor.getValue()) + .isEqualTo("http://category-requests.com/something/primaryAdServer.json"); } @Test @@ -447,7 +517,7 @@ public void getCategoriesShouldReturnFailedFutureWithTimeoutException() { // then assertThat(result.failed()).isTrue(); assertThat(result.cause()).isInstanceOf(TimeoutException.class) - .hasMessage("Failed to fetch categories from url 'http://category-requests/primaryAdServer.json'." + .hasMessage("Failed to fetch categories from url 'http://category-requests.com/something/primaryAdServer.json'." + " Reason: Timeout exceeded"); } @@ -464,7 +534,7 @@ public void getCategoriesShouldReturnFailedFutureWhenResponseStatusIsNot200() { // then assertThat(result.failed()).isTrue(); assertThat(result.cause()).isInstanceOf(PreBidException.class) - .hasMessage("Failed to fetch categories from url 'http://category-requests/primaryAdServer.json'." + .hasMessage("Failed to fetch categories from url 'http://category-requests.com/something/primaryAdServer.json'." + " Reason: Response status code is '400'"); } @@ -481,7 +551,7 @@ public void getCategoriesShouldReturnFailedFutureWhenBodyIsNull() { // then assertThat(result.failed()).isTrue(); assertThat(result.cause()).isInstanceOf(PreBidException.class) - .hasMessage("Failed to fetch categories from url 'http://category-requests/primaryAdServer.json'." + .hasMessage("Failed to fetch categories from url 'http://category-requests.com/something/primaryAdServer.json'." + " Reason: Response body is null or empty"); } @@ -499,7 +569,7 @@ public void getCategoriesShouldReturnFailedFutureWhenBodyCantBeParsed() { assertThat(result.failed()).isTrue(); assertThat(result.cause()).isInstanceOf(PreBidException.class) .hasMessageStartingWith("Failed to fetch categories from url " - + "'http://category-requests/primaryAdServer.json'. Reason: Failed to decode response body"); + + "'http://category-requests.com/something/primaryAdServer.json'. Reason: Failed to decode response body"); } @Test diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json new file mode 100644 index 00000000000..6dd62c37a6d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "madsense": { + "company_id": "companyId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json new file mode 100644 index 00000000000..cab85202f9e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-auction-madsense-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "adid", + "cid": "cid", + "crid": "crid", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "madsense" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "madsense", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "madsense": "{{ madsense.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json new file mode 100644 index 00000000000..69b9eb55ae2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "company_id": "companyId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher" : { + "domain" : "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-response.json new file mode 100644 index 00000000000..0720cdf8a5e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/madsense/test-madsense-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "crid": "crid", + "adid": "adid", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "cid", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json index 0a3824d6e31..9ee42b27d18 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json @@ -12,7 +12,9 @@ ] }, "ext": { - "zeta_global_ssp": {} + "zeta_global_ssp": { + "sid": 11 + } } } ], @@ -37,8 +39,6 @@ "tid": "tid" }, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json index 2608812c09e..2f86fcc4a79 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json @@ -11,11 +11,7 @@ } ] }, - "secure": 1, - "ext": { - "tid": "${json-unit.any-string}", - "bidder": {} - } + "secure": 1 } ], "site": { @@ -47,9 +43,7 @@ "tid": "tid" }, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json index c31fabcb822..39d74ae42cd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json @@ -12,9 +12,15 @@ "cid": "cid001", "adm": "adm001", "h": 250, - "w": 300 + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } } - ] + ], + "seat": "zeta_global_ssp" } ] } diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 01ff79b8691..49c87e9bb84 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -14,8 +14,6 @@ adapters.generic.aliases.cwire.enabled=true adapters.generic.aliases.cwire.endpoint=http://localhost:8090/cwire-exchange adapters.generic.aliases.infytv.enabled=true adapters.generic.aliases.infytv.endpoint=http://localhost:8090/infytv-exchange -adapters.generic.aliases.zeta_global_ssp.enabled=true -adapters.generic.aliases.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange adapters.aceex.enabled=true adapters.aceex.endpoint=http://localhost:8090/aceex-exchange adapters.acuityads.enabled=true @@ -353,6 +351,8 @@ adapters.lunamedia.enabled=true adapters.lunamedia.endpoint=http://localhost:8090/lunamedia-exchange?pubid= adapters.mabidder.enabled=true adapters.mabidder.endpoint=http://localhost:8090/mabidder-exchange +adapters.madsense.enabled=true +adapters.madsense.endpoint=http://localhost:8090/madsense-exchange adapters.madvertise.enabled=true adapters.madvertise.endpoint=http://localhost:8090/madvertise-exchange adapters.marsmedia.enabled=true @@ -624,6 +624,8 @@ adapters.zentotem.enabled=true adapters.zentotem.endpoint=http://localhost:8090/zentotem-exchange adapters.zeroclickfraud.enabled=true adapters.zeroclickfraud.endpoint=http://{{Host}}/zeroclickfraud-exchange?sid={{SourceId}} +adapters.zeta_global_ssp.enabled=true +adapters.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange adapters.aax.enabled=true adapters.aax.endpoint=http://localhost:8090/aax-exchange adapters.zmaticoo.enabled=true