From fcf81d1b83d8b4731a3c7bf2cf7e2e84167b4acb Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Mon, 1 Mar 2021 15:31:27 +0200 Subject: [PATCH 1/3] Improvedigital bidder: Functionality update --- .../improvedigital/ImprovedigitalBidder.java | 89 +++++++++++++++++- .../ImprovedigitalBidderTest.java | 90 ++++++++++++++++++- 2 files changed, 172 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java index ec51f890f44..1a064e06f78 100644 --- a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java @@ -1,15 +1,98 @@ package org.prebid.server.bidder.improvedigital; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.OpenrtbBidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * ImproveDigital {@link Bidder} implementation. */ -public class ImprovedigitalBidder extends OpenrtbBidder { +public class ImprovedigitalBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; public ImprovedigitalBidder(String endpointUrl, JacksonMapper mapper) { - super(endpointUrl, RequestCreationStrategy.SINGLE_REQUEST, Void.class, mapper); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + + return Result.withValues(Collections.singletonList( + HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .payload(request) + .body(mapper.encode(request)) + .build())); + } + + @Override + public Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + if (bidResponse.getSeatbid().size() > 1) { + throw new PreBidException(String.format("Unexpected SeatBid! Must be only one but have: %d", + bidResponse.getSeatbid().size())); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getBanner() != null) { + return BidType.banner; + } + if (imp.getVideo() != null) { + return BidType.video; + } + throw new PreBidException(String.format("Unknown impression type for ID: \"%s\"", impId)); + } + } + throw new PreBidException(String.format("Failed to find impression for ID: \"%s\"", impId)); } } diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java index b8e41ba5496..0fa2ec90752 100644 --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java @@ -1,8 +1,10 @@ package org.prebid.server.bidder.improvedigital; import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -20,10 +22,12 @@ import java.util.List; import java.util.function.Function; +import static java.util.Arrays.asList; 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.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; public class ImprovedigitalBidderTest extends VertxTest { @@ -57,8 +61,8 @@ public void makeHttpRequestsShouldNotModifyIncomingRequest() { // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .containsOnly(bidRequest); + .extracting(HttpRequest::getPayload) + .containsExactly(bidRequest); } @Test @@ -105,7 +109,28 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso } @Test - public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException { + public void makeBidsShouldReturnErrorWhenSeatBidsCountIsMoreThanOne() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().seatbid(asList(SeatBid.builder() + .bid(singletonList(Bid.builder().build())) + .build(), + SeatBid.builder() + .bid(singletonList(Bid.builder().build())) + .build() + )).build())); + + // when + final Result> result = improvedigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(BidderError.badServerResponse("Unexpected SeatBid! Must be only one but have: 2")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfBannerOrVideoNotPresent() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() @@ -117,10 +142,67 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi // when final Result> result = improvedigitalBidder.makeBids(httpCall, null); + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Unknown impression type for ID: \"123\"")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = improvedigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = improvedigitalBidder.makeBids(httpCall, null); + // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresent2() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("456")))); + + // when + final Result> result = improvedigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Failed to find impression for ID: \"456\"")); + assertThat(result.getValue()).isEmpty(); } private static BidResponse givenBidResponse(Function bidCustomizer) { From 67476ad120b5e866e6bcd409d3a7bd17d4fcb336 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Tue, 2 Mar 2021 12:55:51 +0200 Subject: [PATCH 2/3] Naming fix --- .../server/bidder/improvedigital/ImprovedigitalBidderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java index 0fa2ec90752..7b053b53fb5 100644 --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java @@ -187,7 +187,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing } @Test - public void makeBidsShouldReturnVideoBidIfVideoIsPresent2() throws JsonProcessingException { + public void makeBidsShouldReturnErrorIfImpNotFoundForId() throws JsonProcessingException { // given final HttpCall httpCall = givenHttpCall( BidRequest.builder() From 311932ba9b6b198bbf64b8dc69b0ffe69af9b8cb Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Thu, 18 Mar 2021 14:30:59 +0200 Subject: [PATCH 3/3] Improve Digital adapter: add support for native ads --- .../improvedigital/ImprovedigitalBidder.java | 3 +++ .../bidder-config/improvedigital.yaml | 2 ++ .../ImprovedigitalBidderTest.java | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java index 1a064e06f78..c02e876a89c 100644 --- a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java @@ -90,6 +90,9 @@ private static BidType getType(String impId, List imps) { if (imp.getVideo() != null) { return BidType.video; } + if (imp.getXNative() != null) { + return BidType.xNative; + } throw new PreBidException(String.format("Unknown impression type for ID: \"%s\"", impId)); } } diff --git a/src/main/resources/bidder-config/improvedigital.yaml b/src/main/resources/bidder-config/improvedigital.yaml index 776c1565851..ae993af536c 100644 --- a/src/main/resources/bidder-config/improvedigital.yaml +++ b/src/main/resources/bidder-config/improvedigital.yaml @@ -12,9 +12,11 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 253 usersync: diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java index 7b053b53fb5..97358df6662 100644 --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java @@ -4,6 +4,7 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; @@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; public class ImprovedigitalBidderTest extends VertxTest { @@ -186,6 +188,25 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessing .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); } + @Test + public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException { + // given + final HttpCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = improvedigitalBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + } + @Test public void makeBidsShouldReturnErrorIfImpNotFoundForId() throws JsonProcessingException { // given