diff --git a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java index 2ef79d3bfd4..6cdae97b5cb 100644 --- a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java +++ b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java @@ -44,6 +44,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; public class OpenxBidder implements Bidder { @@ -72,9 +73,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ .collect(Collectors.groupingBy(OpenxBidder::resolveImpType)); final List processingErrors = new ArrayList<>(); - final List outgoingRequests = makeRequests(bidRequest, + final List outgoingRequests = makeRequests( + bidRequest, differentiatedImps.get(OpenxImpType.banner), - differentiatedImps.get(OpenxImpType.video), processingErrors); + differentiatedImps.get(OpenxImpType.video), + differentiatedImps.get(OpenxImpType.xNative), + processingErrors); final List errors = errors(differentiatedImps.get(OpenxImpType.other), processingErrors); @@ -101,13 +105,21 @@ public Result> makeBids(BidderCall httpCall, BidRequ return Result.withError(BidderError.generic("Deprecated adapter method invoked")); } - private List makeRequests(BidRequest bidRequest, List bannerImps, List videoImps, - List errors) { + private List makeRequests( + BidRequest bidRequest, + List bannerImps, + List videoImps, + List nativeImps, + List errors) { final List bidRequests = new ArrayList<>(); - // single request for all banner imps - final BidRequest bannerRequest = createSingleRequest(bannerImps, bidRequest, errors); - if (bannerRequest != null) { - bidRequests.add(bannerRequest); + // single request for all banner and native imps + final List bannerAndNativeImps = Stream.of(bannerImps, nativeImps) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .toList(); + final BidRequest bannerAndNativeImpsRequest = createSingleRequest(bannerAndNativeImps, bidRequest, errors); + if (bannerAndNativeImpsRequest != null) { + bidRequests.add(bannerAndNativeImpsRequest); } if (CollectionUtils.isNotEmpty(videoImps)) { @@ -128,16 +140,33 @@ private static OpenxImpType resolveImpType(Imp imp) { if (imp.getVideo() != null) { return OpenxImpType.video; } + if (imp.getXNative() != null) { + return OpenxImpType.xNative; + } return OpenxImpType.other; } + private static BidType resolveBidType(Imp imp) { + if (imp.getBanner() != null) { + return BidType.banner; + } + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return BidType.banner; + } + private List errors(List notSupportedImps, List processingErrors) { final List errors = new ArrayList<>(); // add errors for imps with unsupported media types if (CollectionUtils.isNotEmpty(notSupportedImps)) { errors.addAll( notSupportedImps.stream() - .map(imp -> "OpenX only supports banner and video imps. Ignoring imp id=" + imp.getId()) + .map(imp -> + "OpenX only supports banner, video and native imps. Ignoring imp id=" + imp.getId()) .map(BidderError::badInput) .toList()); } @@ -276,7 +305,7 @@ private static ExtBidPrebidVideo getVideoInfo(Bid bid) { private static Map impIdToBidType(BidRequest bidRequest) { return bidRequest.getImp().stream() - .collect(Collectors.toMap(Imp::getId, imp -> imp.getBanner() != null ? BidType.banner : BidType.video)); + .collect(Collectors.toMap(Imp::getId, OpenxBidder::resolveBidType)); } private static BidType getBidType(Bid bid, Map impIdToBidType) { diff --git a/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java b/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java index 7d9dfb4e5d5..c872e7f97e6 100644 --- a/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java +++ b/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java @@ -3,7 +3,7 @@ public enum OpenxImpType { // supported - banner, video, + banner, video, xNative, // not supported other } diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml index 9e8454131d4..a504fcdf8fd 100644 --- a/src/main/resources/bidder-config/openx.yaml +++ b/src/main/resources/bidder-config/openx.yaml @@ -8,9 +8,11 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 69 usersync: diff --git a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java index 1f218984f91..cbe1e22c7ca 100644 --- a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java @@ -91,30 +91,10 @@ public void makeHttpRequestsShouldReturnResultWithErrorWhenAudioImpsPresent() { assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(2) .containsExactly( - BidderError.badInput("OpenX only supports banner and video imps. Ignoring imp id=impId1"), BidderError.badInput( - "OpenX only supports banner and video imps. Ignoring imp id=impId2")); - } - - @Test - public void makeHttpRequestsShouldReturnResultWithErrorWhenNativeImpsPresent() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(asList( - Imp.builder().id("impId1").xNative(Native.builder().build()).build(), - Imp.builder().id("impId2").xNative(Native.builder().build()).build())) - .build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(2) - .containsExactly( - BidderError.badInput("OpenX only supports banner and video imps. Ignoring imp id=impId1"), + "OpenX only supports banner, video and native imps. Ignoring imp id=impId1"), BidderError.badInput( - "OpenX only supports banner and video imps. Ignoring imp id=impId2")); + "OpenX only supports banner, video and native imps. Ignoring imp id=impId2")); } @Test @@ -254,7 +234,7 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() { // then assertThat(result.getErrors()).hasSize(1) .containsExactly(BidderError.badInput( - "OpenX only supports banner and video imps. Ignoring imp id=impId1")); + "OpenX only supports banner, video and native imps. Ignoring imp id=impId1")); assertThat(result.getValue()).hasSize(3) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) @@ -344,6 +324,158 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() { .build()); } + @Test + public void makeHttpRequestsShouldReturnResultWithSingleBidRequestForMultipleBannerAndNativeImps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(asList( + Imp.builder() + .id("impId4") + .banner(Banner.builder().build()) + .ext(mapper.valueToTree( + ExtPrebid.of(null, + ExtImpOpenx.builder() + .customParams(givenCustomParams("foo4", "bar4")) + .delDomain("se-demo-d.openx.net") + .unit("4").build()))).build(), + Imp.builder() + .id("impId5") + .xNative(Native.builder().request("{\"testreq\":1}").build()) + .ext(mapper.valueToTree( + ExtPrebid.of(null, + ExtImpOpenx.builder() + .customParams(givenCustomParams("foo5", "bar5")) + .delDomain("se-demo-d.openx.net") + .unit("5").build()))).build(), + Imp.builder() + .id("impId6") + .xNative(Native.builder().build()) + .ext(mapper.valueToTree( + ExtPrebid.of(null, + ExtImpOpenx.builder() + .customParams(givenCustomParams("foo6", "bar6")) + .delDomain("se-demo-d.openx.net") + .unit("6").build()))).build())) + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .containsExactly( + // check if all native and banner imps are part of single bidRequest + BidRequest.builder() + .id("bidRequestId") + .imp(asList( + Imp.builder() + .id("impId4") + .tagid("4") + .banner(Banner.builder().build()) + .ext(mapper.valueToTree( + ExtImpOpenx.builder() + .customParams( + givenCustomParams("foo4", "bar4")) + .build())) + .build(), + Imp.builder() + .id("impId5") + .tagid("5") + .xNative(Native.builder().request("{\"testreq\":1}").build()) + .ext(mapper.valueToTree( + ExtImpOpenx.builder() + .customParams( + givenCustomParams("foo5", "bar5")) + .build())) + .build(), + Imp.builder() + .id("impId6") + .tagid("6") + .xNative(Native.builder().build()) + .ext(mapper.valueToTree( + ExtImpOpenx.builder() + .customParams( + givenCustomParams("foo6", "bar6")) + .build())) + .build())) + .ext(jacksonMapper.fillExtension( + ExtRequest.empty(), + OpenxRequestExt.of("se-demo-d.openx.net", null, "hb_pbs_1.0.0"))) + .user(User.builder() + .ext(ExtUser.builder().consent("consent").build()) + .build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) + .build()); + } + + @Test + public void makeHttpRequestsShouldReturnResultWithSingleBidRequestForMultiFormatImps() { + // given + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(asList( + Imp.builder() + .id("impId1") + .banner(Banner.builder().w(320).h(200).build()) + .video(Video.builder().maxduration(10).build()) + .ext(mapper.valueToTree( + ExtPrebid.of(null, ExtImpOpenx.builder().unit("1").build()))) + .build(), + Imp.builder() + .id("impId2") + .banner(Banner.builder().w(300).h(150).build()) + .xNative(Native.builder().request("{\"version\":1}").build()) + .ext(mapper.valueToTree( + ExtPrebid.of(null, ExtImpOpenx.builder().unit("2").build()))) + .build())) + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .containsExactly( + // check if all native and banner imps are part of single bidRequest + BidRequest.builder() + .id("bidRequestId") + .imp(asList( + // verify banner and video media types are preserved in a single imp + Imp.builder() + .id("impId1") + .tagid("1") + .banner(Banner.builder().w(320).h(200).build()) + .video(Video.builder().maxduration(10).build()) + .ext(mapper.valueToTree(ExtImpOpenx.builder().build())).build(), + // verify banner and native media types are preserved in a single imp + Imp.builder() + .id("impId2") + .tagid("2") + .banner(Banner.builder().w(300).h(150).build()) + .xNative(Native.builder().request("{\"version\":1}").build()) + .ext(mapper.valueToTree(ExtImpOpenx.builder().build())) + .build())) + .ext(jacksonMapper.fillExtension( + ExtRequest.empty(), + OpenxRequestExt.of(null, null, "hb_pbs_1.0.0"))) + .user(User.builder() + .ext(ExtUser.builder().consent("consent").build()) + .build()) + .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build()) + .build()); + } + @Test public void makeHttpRequestsShouldPassThroughImpExt() { // given @@ -523,6 +655,48 @@ public void makeBidsShouldReturnResultWithExpectedFields() throws JsonProcessing .build()); } + @Test + public void makeBidsShouldReturnResultForNativeBidsWithExpectedFields() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(OpenxBidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder() + .w(200) + .h(150) + .price(BigDecimal.ONE) + .impid("impId1") + .adm("{\"ver\":\"1.2\"}") + .build())) + .build())) + .cur("UAH") + .ext(OpenxBidResponseExt.of(Map.of("impId1", mapper.createObjectNode().put("somevalue", 1)))) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(singletonList(Imp.builder() + .id("impId1") + .xNative(Native.builder().request("{\"ver\":\"1.2\",\"plcmttype\":3}").build()) + .build())) + .build(); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1) + .containsOnly(BidderBid.of( + Bid.builder() + .impid("impId1") + .price(BigDecimal.ONE) + .w(200) + .h(150) + .adm("{\"ver\":\"1.2\"}") + .build(), + BidType.xNative, "UAH")); + } + @Test public void makeBidsShouldReturnVideoInfoWhenAvailable() throws JsonProcessingException { // given