Skip to content

Commit

Permalink
Vrtcal: Add Native Support (#2737)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Nov 13, 2023
1 parent d006da3 commit 239f12e
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 119 deletions.
41 changes: 19 additions & 22 deletions src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.prebid.server.bidder.vrtcal;

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;
Expand Down Expand Up @@ -46,55 +45,53 @@ public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, B
try {
final List<BidderError> errors = new ArrayList<>();
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors);
return Result.of(extractBids(bidResponse, errors), errors);
} catch (DecodeException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse, List<BidderError> errors) {
private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}
return bidsFromResponse(bidRequest, bidResponse, errors);
return bidsFromResponse(bidResponse, errors);
}

private static List<BidderBid> bidsFromResponse(BidRequest bidRequest,
BidResponse bidResponse,
List<BidderError> errors) {
private static List<BidderBid> bidsFromResponse(BidResponse bidResponse, List<BidderError> errors) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.flatMap(Collection::stream)
.map(bid -> resolveBidderBid(bid, bidResponse.getCur(), bidRequest.getImp(), errors))
.map(bid -> resolveBidderBid(bid, bidResponse.getCur(), errors))
.filter(Objects::nonNull)
.toList();
}

private static BidderBid resolveBidderBid(Bid bid, String currency, List<Imp> imps, List<BidderError> errors) {
private static BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) {
final BidType bidType;
try {
bidType = getBidType(bid.getImpid(), imps);
bidType = getBidMediaType(bid);
} catch (PreBidException e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
return null;
}
return BidderBid.of(bid, bidType, currency);
}

private static BidType getBidType(String impId, List<Imp> 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("Unknown impression type for ID: \"%s\"".formatted(impId));
}
private static BidType getBidMediaType(Bid bid) {
final Integer markupType = bid.getMtype();
if (markupType == null) {
throw new PreBidException("Missing MType for bid: " + bid.getId());
}
throw new PreBidException("Failed to find impression for ID: \"%s\"".formatted(impId));

return switch (markupType) {
case 1 -> BidType.banner;
case 2 -> BidType.video;
case 4 -> BidType.xNative;
default -> throw new PreBidException(
"Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid());
};
}
}

14 changes: 14 additions & 0 deletions src/main/resources/bidder-config/vrtcal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ adapters:
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 706
usersync:
cookie-family-name: vrtcal
redirect:
url: https://usync.vrtcal.com/i?ssp=1804&synctype=redirect&us_privacy={{us_privacy}}&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&surl={{redirect_url}}
support-cors: false
uid-macro: '$$VRTCALUSER$$'
iframe:
url: https://usync.vrtcal.com/i?ssp=1804&synctype=iframe&us_privacy={{us_privacy}}&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&surl={{redirect_url}}
support-cors: false
uid-macro: '$$VRTCALUSER$$'
131 changes: 36 additions & 95 deletions src/test/java/org/prebid/server/bidder/vrtcal/VrtcalBidderTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package org.prebid.server.bidder.vrtcal;

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.Native;
import com.iab.openrtb.request.Video;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
Expand Down Expand Up @@ -33,6 +30,7 @@
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;

public class VrtcalBidderTest extends VertxTest {

Expand Down Expand Up @@ -81,7 +79,7 @@ public void makeHttpRequestShouldReturnCorrectHeaders() {
@Test
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(null, "invalid");
final BidderCall<BidRequest> httpCall = givenHttpCall("invalid");

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
Expand All @@ -93,25 +91,10 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(null,
mapper.writeValueAsString(null));

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(null,
mapper.writeValueAsString(BidResponse.builder().build()));
final BidderCall<BidRequest> httpCall = givenHttpCall();

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
Expand All @@ -122,114 +105,67 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso
}

@Test
public void makeBidsShouldReturnErrorIfImpNotFoundForId() throws JsonProcessingException {
// given
final BidderCall<BidRequest> 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<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors())
.containsExactly(BidderError.badServerResponse("Failed to find impression for ID: \"456\""));
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldReturnErrorIfBannerOrVideoNotPresent() throws JsonProcessingException {
public void makeBidsShouldReturnErrorWhenBidIsAudio() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(
BidRequest.builder()
.imp(singletonList(Imp.builder().id("123").build()))
.build(),
mapper.writeValueAsString(
givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
final Bid audioBid = Bid.builder().impid("imp_id").mtype(3).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(audioBid);

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors())
.containsExactly(BidderError.badServerResponse("Unknown impression type for ID: \"123\""));
.containsExactly(BidderError.badServerResponse("Unable to fetch mediaType 3 in multi-format: imp_id"));
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldReturnBidIfBannerIsPresent() throws JsonProcessingException {
public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(
BidRequest.builder()
.imp(singletonList(Imp.builder().banner(Banner.builder().build()).id("123").build()))
.build(),
mapper.writeValueAsString(
givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));

final Bid videoBid = Bid.builder().impid("imp_id").mtype(2).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(videoBid);
// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD"));
assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, video, "USD"));
}

@Test
public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException {
public void makeBidsShouldReturnBannerBid() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(
BidRequest.builder()
.imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
.build(),
mapper.writeValueAsString(
givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
final Bid bannerBid = Bid.builder().impid("imp_id").mtype(1).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(bannerBid);

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, banner, "USD"));
}

@Test
public void makeBidsShouldReturnErrorIfNativeIsInOriginalImp() throws JsonProcessingException {
public void makeBidsShouldReturnNativeBid() throws JsonProcessingException {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(
BidRequest.builder()
.imp(singletonList(Imp.builder().xNative(Native.builder().build()).id("123").build()))
.build(),
mapper.writeValueAsString(
givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
final Bid nativeBid = Bid.builder().impid("imp_id").mtype(4).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(nativeBid);

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors())
.containsExactly(BidderError.badServerResponse("Unknown impression type for ID: \"123\""));
assertThat(result.getValue()).isEmpty();
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsExactly(BidderBid.of(nativeBid, xNative, "USD"));
}

@Test
public void makeBidsShouldReturnErrorsAndResult() throws JsonProcessingException {
// given
final BidResponse bidResponse = BidResponse.builder()
.seatbid(singletonList(SeatBid.builder()
.bid(List.of(Bid.builder().impid("123").build(),
Bid.builder().impid("456").build()))
.build()))
.build();
final BidderCall<BidRequest> httpCall = givenHttpCall(
BidRequest.builder()
.imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build()))
.build(),
mapper.writeValueAsString(bidResponse));
final Bid validBid = Bid.builder().impid("imp_id").mtype(1).build();
final Bid invalidBid = Bid.builder().impid("imp_id2").mtype(3).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(validBid, invalidBid);

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
Expand Down Expand Up @@ -258,18 +194,23 @@ private static Imp givenImp(Function<Imp.ImpBuilder, Imp.ImpBuilder> impCustomiz
.build();
}

private static BidResponse givenBidResponse(Function<Bid.BidBuilder, Bid.BidBuilder> bidCustomizer) {
return BidResponse.builder()
private static String givenBidResponse(Bid... bids) throws JsonProcessingException {
return mapper.writeValueAsString(BidResponse.builder()
.cur("USD")
.seatbid(singletonList(SeatBid.builder()
.bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
.build()))
.build();
.seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build()))
.build());
}

private static BidderCall<BidRequest> givenHttpCall(Bid... bids) throws JsonProcessingException {
return BidderCall.succeededHttp(
HttpRequest.<BidRequest>builder().payload(null).build(),
HttpResponse.of(200, null, givenBidResponse(bids)),
null);
}

private static BidderCall<BidRequest> givenHttpCall(BidRequest bidRequest, String body) {
private static BidderCall<BidRequest> givenHttpCall(String body) {
return BidderCall.succeededHttp(
HttpRequest.<BidRequest>builder().payload(bidRequest).build(),
HttpRequest.<BidRequest>builder().payload(null).build(),
HttpResponse.of(200, null, body),
null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"crid": "crid",
"w": 300,
"h": 250,
"mtype": 1,
"ext": {
"prebid": {
"type": "banner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
"crid": "crid",
"adm": "adm001",
"h": 250,
"w": 300
"w": 300,
"mtype": 1
}
]
}
],
"bidid": "bid_id"
}
}

0 comments on commit 239f12e

Please sign in to comment.