diff --git a/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java index a36057f2c33..67bfec26571 100644 --- a/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java +++ b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java @@ -1,11 +1,11 @@ package org.prebid.server.bidder.smilewanted; +import com.fasterxml.jackson.core.type.TypeReference; 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.MultiMap; -import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -13,9 +13,13 @@ import org.prebid.server.bidder.model.BidderError; 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.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.smilewanted.ExtImpSmilewanted; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import java.util.Collections; @@ -27,6 +31,11 @@ public class SmileWantedBidder implements Bidder { private static final String SW_INTEGRATION_TYPE = "prebid_server"; private static final String X_OPENRTB_VERSION = "2.5"; private static final int DEFAULT_AT = 1; + private static final String ZONE_ID_MACRO = "{{ZoneId}}"; + + private static final TypeReference> SMILEWANTED_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; private final String endpointUrl; private final JacksonMapper mapper; @@ -38,15 +47,30 @@ public SmileWantedBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { + final ExtImpSmilewanted extImpSmilewanted; + + try { + extImpSmilewanted = parseImpExt(request.getImp().getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + final BidRequest outgoingRequest = request.toBuilder().at(DEFAULT_AT).build(); + final String url = endpointUrl.replace(ZONE_ID_MACRO, HttpUtil.encodeUrl(extImpSmilewanted.getZoneId())); - return Result.withValue(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(createHeaders()) - .payload(outgoingRequest) - .body(mapper.encodeToBytes(outgoingRequest)) - .build()); + return Result.withValue(BidderUtil.defaultRequest( + outgoingRequest, + createHeaders(), + url, + mapper)); + } + + private ExtImpSmilewanted parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), SMILEWANTED_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } } private static MultiMap createHeaders() { diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smilewanted/ExtImpSmilewanted.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smilewanted/ExtImpSmilewanted.java new file mode 100644 index 00000000000..ec08ec3a957 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smilewanted/ExtImpSmilewanted.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.smilewanted; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSmilewanted { + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmileWantedConfiguration.java similarity index 97% rename from src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java rename to src/main/java/org/prebid/server/spring/config/bidder/SmileWantedConfiguration.java index 0b72987e6f9..eb3ae5bb83c 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmileWantedConfiguration.java @@ -17,7 +17,7 @@ @Configuration @PropertySource(value = "classpath:/bidder-config/smilewanted.yaml", factory = YamlPropertySourceFactory.class) -public class SimpleWantedConfiguration { +public class SmileWantedConfiguration { private static final String BIDDER_NAME = "smilewanted"; diff --git a/src/main/resources/bidder-config/smilewanted.yaml b/src/main/resources/bidder-config/smilewanted.yaml index a4dfdea902e..3cac81ccae3 100644 --- a/src/main/resources/bidder-config/smilewanted.yaml +++ b/src/main/resources/bidder-config/smilewanted.yaml @@ -1,6 +1,6 @@ adapters: smilewanted: - endpoint: https://prebid-server.smilewanted.com + endpoint: https://prebid-server.smilewanted.com/java/{{ZoneId}} meta-info: maintainer-email: tech@smilewanted.com app-media-types: diff --git a/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java b/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java index 4dabe7e0bfb..26085bff972 100644 --- a/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smilewanted/SmileWantedBidderTest.java @@ -17,8 +17,12 @@ 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.smilewanted.ExtImpSmilewanted; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -32,7 +36,7 @@ public class SmileWantedBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://{{Host}}/test?param={{PublisherId}}"; + private static final String ENDPOINT_URL = "https://prebid-server.smilewanted.com/java/{{ZoneId}}"; private final SmileWantedBidder target = new SmileWantedBidder(ENDPOINT_URL, jacksonMapper); @@ -41,10 +45,76 @@ public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> new SmileWantedBidder("invalid_url", jacksonMapper)); } + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Missing bidder ext in impression with id: 123"); + }); + } + + @Test + public void makeHttpRequestsShouldReturnSingleRequest() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of( + givenImp("zone123"), + Imp.builder() + .id("456") + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmilewanted.of("zone123")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final HttpRequest httpRequest = result.getValue().get(0); + assertThat(httpRequest.getPayload()).isNotNull(); + assertThat(httpRequest.getPayload().getImp()).hasSize(2); + assertThat(httpRequest.getPayload().getAt()).isEqualTo(1); + assertThat(httpRequest.getImpIds()).containsExactlyInAnyOrder("123", "456"); + } + + @Test + public void makeHttpRequestsShouldBuildCorrectEndpointUrlWithZoneId() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp("zone456"))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://prebid-server.smilewanted.com/java/zone456"); + } + @Test public void makeHttpRequestsShouldCorrectlyAddHeaders() { // given - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp("zone123"))) + .build(); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -65,7 +135,9 @@ public void makeHttpRequestsShouldCorrectlyAddHeaders() { @Test public void makeHttpRequestsShouldSetAtToOne() { // given - final BidRequest bidRequest = BidRequest.builder().build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp("zone123"))) + .build(); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -98,7 +170,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { @Test public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + final BidderCall httpCall = givenHttpCall(null, (BidResponse) null); // when final Result> result = target.makeBids(httpCall, null); @@ -111,8 +183,7 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces @Test public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(BidResponse.builder().build())); + final BidderCall httpCall = givenHttpCall(null, BidResponse.builder().build()); // when final Result> result = target.makeBids(httpCall, null); @@ -130,8 +201,7 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImpAndCorrespon BidRequest.builder() .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); // when final Result> result = target.makeBids(httpCall, null); @@ -148,8 +218,7 @@ public void makeBidsShouldReturnBannerBidIfVideoIsAbsentInRequestImp() throws Js final BidderCall httpCall = givenHttpCall(BidRequest.builder() .imp(singletonList(Imp.builder().id("123").build())) .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); // when final Result> result = target.makeBids(httpCall, null); @@ -160,6 +229,102 @@ public void makeBidsShouldReturnBannerBidIfVideoIsAbsentInRequestImp() throws Js .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), banner, null)); } + @Test + public void makeBidsShouldReturnMultipleBidsFromSingleSeatBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(List.of( + Imp.builder().id("123").build(), + Imp.builder().id("456").video(Video.builder().build()).build())) + .build(), + BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(List.of( + Bid.builder().impid("123").price(BigDecimal.valueOf(1.0)).build(), + Bid.builder().impid("456").price(BigDecimal.valueOf(2.0)).build())) + .build())) + .build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .containsExactlyInAnyOrder( + BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(1.0)).build(), banner, "USD"), + BidderBid.of(Bid.builder().impid("456").price(BigDecimal.valueOf(2.0)).build(), video, "USD")); + } + + @Test + public void makeBidsShouldFilterNullBids() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(Arrays.asList( + Bid.builder().impid("123").build(), + null, + Bid.builder().impid("456").build())) + .build())) + .build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(BidderBid::getBid) + .extracting(Bid::getImpid) + .containsExactlyInAnyOrder("123", "456"); + } + + @Test + public void makeBidsShouldReturnBidWithCurrency() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + BidResponse.builder() + .cur("EUR") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder().impid("123").build())) + .build())) + .build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBidCurrency) + .containsExactly("EUR"); + } + + @Test + public void makeBidsShouldReturnEmptyListIfSeatBidIsEmpty() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().build())) + .build()); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) @@ -173,4 +338,16 @@ private static BidderCall givenHttpCall(BidRequest bidRequest, Strin HttpResponse.of(200, null, body), null); } + + private static BidderCall givenHttpCall(BidRequest bidRequest, BidResponse bidResponse) + throws JsonProcessingException { + return givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + } + + private static Imp givenImp(String zoneId) { + return Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpSmilewanted.of(zoneId)))) + .build(); + } } diff --git a/src/test/java/org/prebid/server/it/SmileWantedTest.java b/src/test/java/org/prebid/server/it/SmileWantedTest.java index 172d2063711..f4d24e0de28 100644 --- a/src/test/java/org/prebid/server/it/SmileWantedTest.java +++ b/src/test/java/org/prebid/server/it/SmileWantedTest.java @@ -18,7 +18,7 @@ public class SmileWantedTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromSmileWanted() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smilewanted-exchange")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smilewanted-exchange/java/someZoneId")) .withRequestBody(equalToJson(jsonFrom("openrtb2/smilewanted/test-smilewanted-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/smilewanted/test-smilewanted-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 33a5bccfe4d..1498164ffc4 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -522,7 +522,7 @@ adapters.smarthub.aliases.artechnology.endpoint=http://localhost:8090/artechnolo adapters.smartyads.enabled=true adapters.smartyads.endpoint=http://localhost:8090/smartyads-exchange adapters.smilewanted.enabled=true -adapters.smilewanted.endpoint=http://localhost:8090/smilewanted-exchange +adapters.smilewanted.endpoint=http://localhost:8090/smilewanted-exchange/java/{{ZoneId}} adapters.smoot.enabled=true adapters.smoot.endpoint=http://localhost:8090/smoot-exchange adapters.smrtconnect.enabled=true