Skip to content

Commit

Permalink
Loyal: New Adapter (#3140)
Browse files Browse the repository at this point in the history
added LoyalBidderTest.java
  • Loading branch information
przemkaczmarek committed May 13, 2024
1 parent b4a19f7 commit 0935323
Show file tree
Hide file tree
Showing 2 changed files with 290 additions and 8 deletions.
21 changes: 14 additions & 7 deletions src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
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.adf.ExtImpAdf;
import org.prebid.server.proto.openrtb.ext.request.loyal.ExtImpLoyal;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.HttpUtil;
Expand All @@ -41,7 +40,7 @@ public class LoyalBidder implements Bidder<BidRequest> {
private final JacksonMapper mapper;

public LoyalBidder(String endpointUrl, JacksonMapper mapper) {
this.endpointUrl = Objects.requireNonNull(endpointUrl);
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.mapper = Objects.requireNonNull(mapper);
}

Expand All @@ -55,8 +54,8 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
final ExtImpLoyal ext = parseImpExt(imp);
final HttpRequest<BidRequest> httpRequest = createHttpRequest(ext, request);
requests.add(httpRequest);
} catch (Exception e) {
errors.add(BidderError.badInput("Failed to parse the ExtImpLoyal object: " + e.getMessage()));
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

Expand All @@ -78,12 +77,16 @@ private ExtImpLoyal parseImpExt(Imp imp) {
}

private HttpRequest<BidRequest> createHttpRequest(ExtImpLoyal ext, BidRequest request) {
String url = null;
String url = endpointUrl;
if (StringUtils.isNotBlank(ext.getPlacementId())) {
url = endpointUrl.replace(PLACEMENT_ID_MACRO, ext.getPlacementId());
url = url.replace(PLACEMENT_ID_MACRO, ext.getPlacementId());
} else {
url = url.replace("param={{PlacementId}}&", ""); // Remove the PlacementId part if not available
}
if (StringUtils.isNotBlank(ext.getEndpointId())) {
url = endpointUrl.replace(ENDPOINT_ID_MACRO, ext.getEndpointId());
url = url.replace(ENDPOINT_ID_MACRO, ext.getEndpointId());
} else {
url = url.replace("&param2={{EndpointId}}", ""); // Remove the EndpointId part if not available
}
final BidRequest outgoingRequest = request.toBuilder().build();
return HttpRequest.<BidRequest>builder()
Expand All @@ -97,6 +100,10 @@ private HttpRequest<BidRequest> createHttpRequest(ExtImpLoyal ext, BidRequest re

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
if (httpCall.getResponse() == null || httpCall.getResponse().getBody() == null) {
return Result.withError(BidderError.badServerResponse("No response or empty body"));
}

try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.withValues(extractBids(bidResponse));
Expand Down
277 changes: 276 additions & 1 deletion src/test/java/org/prebid/server/bidder/loyal/LoyalBidderTest.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,293 @@
package org.prebid.server.bidder.loyal;

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.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.http.HttpMethod;
import org.junit.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.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.between.ExtImpBetween;
import org.prebid.server.proto.openrtb.ext.request.loyal.ExtImpLoyal;
import org.prebid.server.util.HttpUtil;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;

import static java.util.Collections.singletonList;
import static java.util.function.Function.identity;
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.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;

public class LoyalBidderTest extends VertxTest {

private static final String ENDPOINT_URL = "https://test.com/test?param={{PlacementId}}";
private static final String ENDPOINT_URL = "https://test.com/test?param={{PlacementId}}&param2={{EndpointId}}";

private final LoyalBidder target = new LoyalBidder(ENDPOINT_URL, jacksonMapper);

@Test
public void creationShouldFailOnInvalidEndpointUrl() {
assertThatIllegalArgumentException().isThrownBy(() -> new LoyalBidder("invalid_url", jacksonMapper));
}

@Test
public void makeHttpRequestsShouldReturnExpectedBody() {
// given
final BidRequest bidRequest = givenBidRequest();

// when
final Result<List<HttpRequest<BidRequest>>> results = target.makeHttpRequests(bidRequest);

// then
assertThat(results.getValue()).hasSize(1).first().satisfies(request -> assertThat(request.getBody()).isEqualTo(jacksonMapper.encodeToBytes(bidRequest))).satisfies(request -> assertThat(request.getPayload()).isEqualTo(bidRequest));
assertThat(results.getErrors()).isEmpty();
}


@Test
public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() {
// given
final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).hasSize(1);
assertThat(result.getErrors()).containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 123"));
}


@Test
public void shouldReplacePlacementIdMacro() {
// given
final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLoyal.of("placement123", null)))));

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1);
assertThat(result.getValue()).extracting(HttpRequest::getUri).containsExactly("https://test.com/test?param=placement123");
}

@Test
public void shouldReplaceEndpointIdMacro() {
// given
final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLoyal.of(null, "endpoint123")))));

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1);
assertThat(result.getValue()).extracting(HttpRequest::getUri).containsExactly("https://test.com/test?param2=endpoint123");
}

@Test
public void shouldHandleErrorResponse() {
// given
final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLoyal.of("placement123", "endpointId")))));
final HttpRequest<BidRequest> httpRequest = HttpRequest.<BidRequest>builder().method(HttpMethod.POST).uri("https://test.com/test?param=placement123").body(jacksonMapper.encodeToBytes(bidRequest)).headers(HttpUtil.headers()).payload(bidRequest).build();
final BidderCall<BidRequest> httpCall = BidderCall.failedHttp(httpRequest, BidderError.badInput("Bad request"));

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

// then
assertThat(result.getErrors()).isNotEmpty();
assertThat(result.getErrors().get(0).getMessage()).isEqualTo("No response or empty body");
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
// given
final BidderCall<BidRequest> httpCall = givenHttpCall(null, "invalid");

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

// then
assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> {
assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token");
});
assertThat(result.getValue()).isEmpty();
}

@Test
public void makeBidsShouldHandleNullBidResponse() {
// given
final HttpRequest<BidRequest> httpRequest = HttpRequest.<BidRequest>builder().method(HttpMethod.POST).uri("https://test.com").body(new byte[0]).build();
final HttpResponse httpResponse = HttpResponse.of(200, null, "{}");
final BidderCall<BidRequest> httpCall = BidderCall.storedHttp(httpRequest, httpResponse);

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

// then
assertThat(result.getErrors()).isNotEmpty();
assertThat(result.getErrors().get(0).getMessage()).isEqualTo("Empty SeatBid array");
}

@Test
public void makeBidsShouldHandleEmptySeatbid() {
// given
final HttpRequest<BidRequest> httpRequest = HttpRequest.<BidRequest>builder().method(HttpMethod.POST).uri("https://test.com").body(new byte[0]).build();
final HttpResponse httpResponse = HttpResponse.of(200, null, "{\"seatbid\": []}");
final BidderCall<BidRequest> httpCall = BidderCall.storedHttp(httpRequest, httpResponse);

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

// then
assertThat(result.getErrors()).isNotEmpty();
assertThat(result.getErrors().get(0).getMessage()).isEqualTo("Empty SeatBid array");
}

@Test
public void makeBidsShouldReturnAllFourBidTypesSuccessfully() throws JsonProcessingException {
// given
final Bid bannerBid = Bid.builder().impid("1").mtype(1).build();
final Bid videoBid = Bid.builder().impid("2").mtype(2).build();
final Bid audioBid = Bid.builder().impid("3").mtype(3).build();
final Bid nativeBid = Bid.builder().impid("4").mtype(4).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(bannerBid, videoBid, audioBid, nativeBid));

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, banner, "USD"), BidderBid.of(videoBid, video, "USD"), BidderBid.of(audioBid, audio, "USD"), BidderBid.of(nativeBid, xNative, "USD"));
}

@Test
public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException {
// given
final Bid bannerBid = Bid.builder().impid("1").mtype(1).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(bannerBid));

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsOnly(BidderBid.of(bannerBid, banner, "USD"));

}

@Test
public void makeBidsShouldReturnAudioBidSuccessfully() throws JsonProcessingException {
// given
final Bid audioBid = Bid.builder().impid("3").mtype(3).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(audioBid));

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsOnly(BidderBid.of(audioBid, audio, "USD"));
}

@Test
public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException {
// given
final Bid videoBid = Bid.builder().impid("2").mtype(2).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(videoBid));
// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsOnly(BidderBid.of(videoBid, video, "USD"));
}

@Test
public void makeBidsShouldReturnNativeBidSuccessfully() throws JsonProcessingException {
// given
final Bid nativeBid = Bid.builder().impid("4").mtype(4).build();

final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(nativeBid));

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).containsOnly(BidderBid.of(nativeBid, xNative, "USD"));
}

@Test
public void makeBidsShouldReturnErrorWhenImpTypeIsNotSupported() throws JsonProcessingException {
// given
final Bid audioBid = Bid.builder().impid("id").mtype(5).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(audioBid));

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
// then
assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Unable to fetch mediaType 5 in multi-format: id"));
}

@Test
public void makeBidsShouldReturnErrorWhenImpTypeIsNull() throws JsonProcessingException {
// given
final Bid bid = Bid.builder().impid("id").mtype(null).build();
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(bid));

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
// then
assertThat(result.getErrors()).containsExactly(BidderError.badServerResponse("Missing MType for bid: " + bid.getId()));
}


private static BidRequest givenBidRequest() {
final Imp imp = Imp.builder().id("imp_id").ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpLoyal.of("{{PlacementId}}", "endpointId")))).build();
return BidRequest.builder().imp(List.of(imp)).build();
}

private static String givenBidResponse(Bid... bids) throws JsonProcessingException {
return mapper.writeValueAsString(BidResponse.builder().cur("USD").seatbid(bids.length == 0 ? Collections.emptyList() : List.of(SeatBid.builder().bid(List.of(bids)).build())).build());
}

private static BidRequest givenBidRequest(Function<BidRequest.BidRequestBuilder, BidRequest.BidRequestBuilder> bidRequestCustomizer, Function<Imp.ImpBuilder, Imp.ImpBuilder> impCustomizer) {

return bidRequestCustomizer.apply(BidRequest.builder().imp(singletonList(givenImp(impCustomizer)))).build();
}

private static BidRequest givenBidRequest(Function<Imp.ImpBuilder, Imp.ImpBuilder> impCustomizer) {
return givenBidRequest(identity(), impCustomizer);
}

private static Imp givenImp(Function<Imp.ImpBuilder, Imp.ImpBuilder> impCustomizer) {
return impCustomizer.apply(Imp.builder().id("123").banner(Banner.builder().w(23).h(25).build()).ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpBetween.of(null, "pubId"))))).build();
}

private static BidderCall<BidRequest> givenHttpCall(BidRequest bidRequest, String body) {
return BidderCall.succeededHttp(HttpRequest.<BidRequest>builder().payload(bidRequest).build(), HttpResponse.of(200, null, body), null);
}
}

0 comments on commit 0935323

Please sign in to comment.