diff --git a/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java b/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java index 46ed20a06f1..d3851455d1d 100644 --- a/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java +++ b/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java @@ -8,6 +8,7 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; @@ -46,6 +47,8 @@ public class YandexBidder implements Bidder { private static final String PAGE_ID_MACRO = "{{PageId}}"; private static final String IMP_ID_MACRO = "{{ImpId}}"; + private static final String DISPLAY_MANAGER = "prebid.java"; + private static final String DISPLAY_MANAGER_VERSION = "1.1"; private final String endpointUrl; private final JacksonMapper mapper; @@ -110,17 +113,25 @@ private ExtImpYandex parseAndValidateImpExt(ObjectNode impExtNode, final String } private static Imp modifyImp(Imp imp) { - if (imp.getBanner() != null) { - return imp.toBuilder().banner(modifyBanner(imp.getBanner())).build(); - } - if (imp.getXNative() != null) { - return imp; + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { + throw new PreBidException("Imp #%s must contain at least one valid format (banner, video, or native)" + .formatted(imp.getId())); } - throw new PreBidException("Yandex only supports banner and native types. Ignoring imp id #%s" - .formatted(imp.getId())); + + return imp.toBuilder() + .displaymanager(DISPLAY_MANAGER) + .displaymanagerver(DISPLAY_MANAGER_VERSION) + .banner(modifyBanner(imp.getBanner())) + .video(modifyVideo(imp.getVideo())) + .xNative(imp.getXNative()) + .build(); } private static Banner modifyBanner(Banner banner) { + if (banner == null) { + return null; + } + final Integer weight = banner.getW(); final Integer height = banner.getH(); final List format = banner.getFormat(); @@ -134,6 +145,31 @@ private static Banner modifyBanner(Banner banner) { return banner; } + private static Video modifyVideo(Video video) { + if (video == null) { + return null; + } + + final Integer width = video.getW(); + final Integer height = video.getH(); + if (width == null || height == null || width == 0 || height == 0) { + throw new PreBidException("Invalid sizes provided for Video %sx%s".formatted(width, height)); + } + + final Video.VideoBuilder videoBuilder = video.toBuilder(); + if (video.getMinduration() == null || video.getMinduration() == 0) { + videoBuilder.minduration(1); + } + if (video.getMaxduration() == null || video.getMaxduration() == 0) { + videoBuilder.maxduration(120); + } + if (CollectionUtils.isEmpty(video.getProtocols())) { + videoBuilder.protocols(Collections.singletonList(3)); + } + + return videoBuilder.build(); + } + private String modifyUrl(ExtImpYandex extImpYandex, String referer, String currency) { final String resolvedUrl = endpointUrl .replace(PAGE_ID_MACRO, HttpUtil.encodeUrl(extImpYandex.getPageId().toString())) @@ -167,6 +203,10 @@ private HttpRequest buildHttpRequest(BidRequest outgoingRequest, Str private static MultiMap headers(BidRequest bidRequest) { final MultiMap headers = HttpUtil.headers(); + + headers.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, getReferer(bidRequest)); + final Device device = bidRequest.getDevice(); if (device != null) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); @@ -217,18 +257,15 @@ private static BidType getBidType(String bidImpId, List imps) { } private static BidType resolveImpType(Imp imp) { + if (imp.getVideo() != null) { + return BidType.video; + } if (imp.getXNative() != null) { return BidType.xNative; } if (imp.getBanner() != null) { return BidType.banner; } - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getAudio() != null) { - return BidType.audio; - } throw new PreBidException("Processing an invalid impression; cannot resolve impression type for imp #%s" .formatted(imp.getId())); } diff --git a/src/main/resources/bidder-config/yandex.yaml b/src/main/resources/bidder-config/yandex.yaml index f980fe3cb5e..11800ddb11a 100644 --- a/src/main/resources/bidder-config/yandex.yaml +++ b/src/main/resources/bidder-config/yandex.yaml @@ -7,11 +7,12 @@ adapters: app-media-types: site-media-types: - banner + - video - native vendor-id: 0 usersync: cookie-family-name: yandex redirect: - url: https://an.yandex.ru/mapuid/yandex/?ssp-id=10500&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&location={{redirect_url}} + url: https://yandex.ru/an/mapuid/yandex/?ssp-id=10500&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&location={{redirect_url}} support-cors: false uid-macro: '{YANDEXUID}' diff --git a/src/test/java/org/prebid/server/bidder/yandex/YandexBidderTest.java b/src/test/java/org/prebid/server/bidder/yandex/YandexBidderTest.java index dbc3cf23856..26322943187 100644 --- a/src/test/java/org/prebid/server/bidder/yandex/YandexBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yandex/YandexBidderTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -36,7 +35,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; -import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @@ -144,7 +142,7 @@ public void makeHttpRequestsShouldCreateARequestForEachImpAndSkipImpsWithNoBanne final Result>> result = target.makeHttpRequests(bidRequest); // then assertThat(result.getErrors()).containsExactly( - BidderError.badInput("Yandex only supports banner and native types. Ignoring imp id #blockA") + BidderError.badInput("Imp #blockA must contain at least one valid format (banner, video, or native)") ); } @@ -184,7 +182,6 @@ public void makeHttpRequestsShouldReturnErrorWhenBannerHasNoFormats() { final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder.id("blockA").banner(Banner.builder().build()), identity()); - // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -215,6 +212,208 @@ public void makeHttpRequestsSetFirstImpressionBannerWidthAndHeightWhenFromFirstF .containsOnly(tuple(300, 600)); } + @Test + public void makeHttpRequestsShouldReturnErrorWhenVideoWidthIsZero() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.id("blockA").banner(null).video(Video.builder().w(0).h(600).build()), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid sizes provided for Video 0x600")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenVideoHeightIsZero() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.id("blockA").banner(null).video(Video.builder().w(300).h(0).build()), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid sizes provided for Video 300x0")); + } + + @Test + public void makeHttpRequestsShouldModifyVideoParameters() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.id("blockA").banner(null) + .video(Video.builder().w(300).h(600).build()), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getVideo) + .extracting(Video::getMinduration, Video::getMaxduration, Video::getProtocols) + .containsOnly(tuple(1, 120, singletonList(3))); + } + + @Test + public void makeHttpRequestsShouldSetExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), + requestBuilder -> requestBuilder.site(Site.builder().id("1").page("https://example.com/path?query=value").build()) + .device(Device.builder().ua("UA").language("EN").ip("127.0.0.1").build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue().getFirst().getHeaders()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly(tuple("Accept-Language", "EN"), + tuple("User-Agent", "UA"), + tuple("X-Forwarded-For", "127.0.0.1"), + tuple("X-Real-Ip", "127.0.0.1"), + tuple("Content-Type", "application/json;charset=utf-8"), + tuple("Accept", "application/json"), + tuple("x-openrtb-version", "2.5"), + tuple("Referer", "https://example.com/path?query=value")); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(impBuilder -> impBuilder.id("blockA").ext(givenImpExt(1))))) + .site(Site.builder().id("1").page("https://example.com/path?query=value").build()) + .cur(asList("EUR", "USD")) + .build(); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/?" + + "target-ref=https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue&ssp-cur=EUR"); + } + + @Test + public void makeHttpRequestsShouldSupportMultiFormatImpression() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().id("1").build()) + .imp(singletonList( + Imp.builder().id("multiFormatImp") + .banner(Banner.builder().w(300).h(600).build()) + .video(Video.builder().w(300).h(600).build()) + .xNative(Native.builder().build()) + .ext(givenImpExt(1)) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + + final Imp modifiedImp = result.getValue().getFirst().getPayload().getImp().getFirst(); + assertThat(modifiedImp.getBanner()).isNotNull(); + assertThat(modifiedImp.getVideo()).isNotNull(); + assertThat(modifiedImp.getXNative()).isNotNull(); + assertThat(modifiedImp.getDisplaymanager()).isEqualTo("prebid.java"); + assertThat(modifiedImp.getDisplaymanagerver()).isEqualTo("1.1"); + } + + @Test + public void makeHttpRequestsShouldSupportMultiFormatImpressionWithPartialErrors() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().id("1").build()) + .imp(singletonList( + Imp.builder().id("multiFormatImpWithErrors") + .banner(Banner.builder().w(0).h(0).build()) // Invalid banner + .video(Video.builder().w(300).h(600).build()) // Valid video + .xNative(Native.builder().build()) // Valid native + .ext(givenImpExt(1)) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Invalid sizes provided for Banner 0x0")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenNoValidFormats() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().id("1").build()) + .imp(singletonList( + Imp.builder().id("noValidFormats") + .banner(Banner.builder().w(0).h(0).build()) // Invalid banner + .video(Video.builder().w(0).h(0).build()) // Invalid video + .ext(givenImpExt(1)) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badInput("Invalid sizes provided for Banner 0x0")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldSetDisplayManagerAndVersionForAllImpTypes() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().id("1").build()) + .imp(asList( + Imp.builder().id("bannerImp") + .banner(Banner.builder().w(300).h(600).build()) + .ext(givenImpExt(1)) + .build(), + Imp.builder().id("videoImp") + .video(Video.builder().w(300).h(600).build()) + .ext(givenImpExt(2)) + .build(), + Imp.builder().id("nativeImp") + .xNative(Native.builder().build()) + .ext(givenImpExt(3)) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(3) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getDisplaymanager, Imp::getDisplaymanagerver) + .containsOnly( + tuple("prebid.java", "1.1"), + tuple("prebid.java", "1.1"), + tuple("prebid.java", "1.1")); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given @@ -294,18 +493,16 @@ public void makeBidsShouldReturnErrorWhenBidImpIdIsNotPresent() throws JsonProce } @Test - public void makeBidsShouldReturnBannerAndNative() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { // given final BidderCall bidderCall = givenBidderCall( BidRequest.builder() - .imp(asList(Imp.builder().id("blockA").xNative(Native.builder().build()).build(), - Imp.builder().id("blockB").banner(Banner.builder().build()).build())) + .imp(singletonList(Imp.builder().id("blockA").video(Video.builder().build()).build())) .build(), mapper.writeValueAsString(BidResponse.builder() .cur("USD") .seatbid(singletonList(SeatBid.builder() - .bid(asList(Bid.builder().impid("blockA").build(), - Bid.builder().impid("blockB").build())) + .bid(singletonList(Bid.builder().impid("blockA").build())) .build())) .build())); @@ -315,23 +512,20 @@ public void makeBidsShouldReturnBannerAndNative() throws JsonProcessingException // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("blockA").build(), xNative, "USD"), - BidderBid.of(Bid.builder().impid("blockB").build(), banner, "USD")); + .containsOnly(BidderBid.of(Bid.builder().impid("blockA").build(), video, "USD")); } @Test - public void makeBidsShouldReturnVideoAndAudio() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given final BidderCall bidderCall = givenBidderCall( BidRequest.builder() - .imp(asList(Imp.builder().id("blockA").video(Video.builder().build()).build(), - Imp.builder().id("blockB").audio(Audio.builder().build()).build())) + .imp(singletonList(Imp.builder().id("blockB").banner(Banner.builder().build()).build())) .build(), mapper.writeValueAsString(BidResponse.builder() .cur("USD") .seatbid(singletonList(SeatBid.builder() - .bid(asList(Bid.builder().impid("blockA").build(), - Bid.builder().impid("blockB").build())) + .bid(singletonList(Bid.builder().impid("blockB").build())) .build())) .build())); @@ -341,21 +535,20 @@ public void makeBidsShouldReturnVideoAndAudio() throws JsonProcessingException { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("blockA").build(), video, "USD"), - BidderBid.of(Bid.builder().impid("blockB").build(), audio, "USD")); + .containsOnly(BidderBid.of(Bid.builder().impid("blockB").build(), banner, "USD")); } @Test - public void makeBidsShouldReturnError() throws JsonProcessingException { + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { // given final BidderCall bidderCall = givenBidderCall( BidRequest.builder() - .imp(singletonList(Imp.builder().id("blockA").build())) + .imp(singletonList(Imp.builder().id("blockC").xNative(Native.builder().build()).build())) .build(), mapper.writeValueAsString(BidResponse.builder() .cur("USD") .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(Bid.builder().impid("blockA").build())) + .bid(singletonList(Bid.builder().impid("blockC").build())) .build())) .build())); @@ -363,50 +556,57 @@ public void makeBidsShouldReturnError() throws JsonProcessingException { final Result> result = target.makeBids(bidderCall, null); // then - assertThat(result.getErrors()).containsExactly( - BidderError.badServerResponse( - "Processing an invalid impression; cannot resolve impression type for imp #blockA")); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("blockC").build(), xNative, "USD")); } @Test - public void makeHttpRequestsShouldSetExpectedHeaders() { + public void makeBidsShouldReturnError() throws JsonProcessingException { // given - final BidRequest bidRequest = givenBidRequest(identity(), - requestBuilder -> requestBuilder.site(null) - .device(Device.builder().ua("UA").language("EN").ip("127.0.0.1").build())); + final BidderCall bidderCall = givenBidderCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("blockA").build())) + .build(), + mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder().impid("blockA").build())) + .build())) + .build())); // when - final Result>> result = target.makeHttpRequests(bidRequest); + final Result> result = target.makeBids(bidderCall, null); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue().getFirst().getHeaders()) - .extracting(Map.Entry::getKey, Map.Entry::getValue) - .containsOnly(tuple("Accept-Language", "EN"), - tuple("User-Agent", "UA"), - tuple("X-Forwarded-For", "127.0.0.1"), - tuple("X-Real-Ip", "127.0.0.1"), - tuple("Content-Type", "application/json;charset=utf-8"), - tuple("Accept", "application/json")); + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse( + "Processing an invalid impression; cannot resolve impression type for imp #blockA")); + assertThat(result.getValue()).isEmpty(); } @Test - public void makeHttpRequestsShouldCreateCorrectURL() { + public void makeBidsShouldReturnCorrectBidTypeForMultiFormatImpression() throws JsonProcessingException { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder.id("blockA").ext(givenImpExt(1))))) - .site(Site.builder().id("1").page("https://domain.com/").build()) - .cur(asList("EUR", "USD")) + .imp(singletonList( + Imp.builder().id("multiFormatImp") + .banner(Banner.builder().w(300).h(600).build()) + .video(Video.builder().w(300).h(600).build()) + .xNative(Native.builder().build()) + .build())) .build(); + + final BidResponse bidResponse = givenBidResponse(bidBuilder -> bidBuilder.impid("multiFormatImp")); + // when - final Result>> result = target.makeHttpRequests(bidRequest); + final Result> result = target.makeBids( + givenBidderCall(bidRequest, mapper.writeValueAsString(bidResponse)), bidRequest); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).extracting(HttpRequest::getUri) - .containsExactly("https://test.endpoint.com/?" - + "target-ref=https%3A%2F%2Fdomain.com%2F&ssp-cur=EUR"); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getType()).isEqualTo(video); // Video has highest priority } private static BidRequest givenBidRequest( diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-yandex-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-yandex-bid-request.json index c833c0633f5..4db9a09b23b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-yandex-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-yandex-bid-request.json @@ -8,6 +8,8 @@ "w": 300, "h": 600 }, + "displaymanager" : "prebid.java", + "displaymanagerver" : "1.1", "ext": { "tid": "${json-unit.any-string}", "bidder": {