From 0935323422115c251ed31264f12ecb4a4d12d47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Kaczmarek?= Date: Mon, 13 May 2024 12:47:30 +0200 Subject: [PATCH] Loyal: New Adapter (#3140) added LoyalBidderTest.java --- .../server/bidder/loyal/LoyalBidder.java | 21 +- .../server/bidder/loyal/LoyalBidderTest.java | 277 +++++++++++++++++- 2 files changed, 290 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java b/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java index 3a6df3c95cc..0316ef6309a 100644 --- a/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java +++ b/src/main/java/org/prebid/server/bidder/loyal/LoyalBidder.java @@ -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; @@ -41,7 +40,7 @@ public class LoyalBidder implements Bidder { 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); } @@ -55,8 +54,8 @@ public Result>> makeHttpRequests(BidRequest request final ExtImpLoyal ext = parseImpExt(imp); final HttpRequest 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())); } } @@ -78,12 +77,16 @@ private ExtImpLoyal parseImpExt(Imp imp) { } private HttpRequest 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("¶m2={{EndpointId}}", ""); // Remove the EndpointId part if not available } final BidRequest outgoingRequest = request.toBuilder().build(); return HttpRequest.builder() @@ -97,6 +100,10 @@ private HttpRequest createHttpRequest(ExtImpLoyal ext, BidRequest re @Override public Result> makeBids(BidderCall 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)); diff --git a/src/test/java/org/prebid/server/bidder/loyal/LoyalBidderTest.java b/src/test/java/org/prebid/server/bidder/loyal/LoyalBidderTest.java index fefc457b222..a5454e57d5f 100644 --- a/src/test/java/org/prebid/server/bidder/loyal/LoyalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/loyal/LoyalBidderTest.java @@ -1,13 +1,42 @@ 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}}¶m2={{EndpointId}}"; private final LoyalBidder target = new LoyalBidder(ENDPOINT_URL, jacksonMapper); @@ -15,4 +44,250 @@ public class LoyalBidderTest extends VertxTest { public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> new LoyalBidder("invalid_url", jacksonMapper)); } + + @Test + public void makeHttpRequestsShouldReturnExpectedBody() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> 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>> 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>> 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>> 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 httpRequest = HttpRequest.builder().method(HttpMethod.POST).uri("https://test.com/test?param=placement123").body(jacksonMapper.encodeToBytes(bidRequest)).headers(HttpUtil.headers()).payload(bidRequest).build(); + final BidderCall httpCall = BidderCall.failedHttp(httpRequest, BidderError.badInput("Bad request")); + + // when + final Result> 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 httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> 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 httpRequest = HttpRequest.builder().method(HttpMethod.POST).uri("https://test.com").body(new byte[0]).build(); + final HttpResponse httpResponse = HttpResponse.of(200, null, "{}"); + final BidderCall httpCall = BidderCall.storedHttp(httpRequest, httpResponse); + + // when + final Result> 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 httpRequest = HttpRequest.builder().method(HttpMethod.POST).uri("https://test.com").body(new byte[0]).build(); + final HttpResponse httpResponse = HttpResponse.of(200, null, "{\"seatbid\": []}"); + final BidderCall httpCall = BidderCall.storedHttp(httpRequest, httpResponse); + + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(bannerBid, videoBid, audioBid, nativeBid)); + + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(bannerBid)); + + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(audioBid)); + + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(videoBid)); + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(nativeBid)); + + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(audioBid)); + + // when + final Result> 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 httpCall = givenHttpCall(givenBidRequest(), givenBidResponse(bid)); + + // when + final Result> 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 bidRequestCustomizer, Function impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder().imp(singletonList(givenImp(impCustomizer)))).build(); + } + + private static BidRequest givenBidRequest(Function impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(Function 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 givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp(HttpRequest.builder().payload(bidRequest).build(), HttpResponse.of(200, null, body), null); + } }