diff --git a/src/main/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidder.java b/src/main/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidder.java index 3bc78b588b0..d0ab8651bc3 100644 --- a/src/main/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidder.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.resetdigital; +import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; @@ -12,171 +13,165 @@ 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.Price; import org.prebid.server.bidder.model.Result; -import org.prebid.server.currency.CurrencyConversionService; 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.resetdigital.ExtImpResetDigital; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Stream; public class ResetDigitalBidder implements Bidder { private static final String DEFAULT_CURRENCY = "USD"; + private static final TypeReference> EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; private final String endpointUrl; - private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public ResetDigitalBidder(String endpointUrl, - CurrencyConversionService currencyConversionService, - JacksonMapper mapper) { - + public ResetDigitalBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { - final List bannerImps = new ArrayList<>(); - final List videoImps = new ArrayList<>(); - final List audioImps = new ArrayList<>(); - Price bidFloorPrice; - - for (Imp imp : request.getImp()) { - try { - bidFloorPrice = resolveBidFloor(imp, request); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - populateBannerImps(bannerImps, bidFloorPrice, imp); - populateVideoImps(videoImps, bidFloorPrice, imp); - populateAudiImps(audioImps, bidFloorPrice, imp); + if (CollectionUtils.isEmpty(request.getImp()) || request.getImp().size() != 1) { + return Result.withError(BidderError.badInput( + "ResetDigital adapter supports only one impression per request")); } - return Result.withValues(getHttpRequests(request, bannerImps, videoImps, audioImps)); - } - - private List> getHttpRequests(BidRequest request, - List bannerImps, - List videoImps, - List audioImps) { - - return Stream.of(bannerImps, videoImps, audioImps) - .filter(CollectionUtils::isNotEmpty) - .map(imp -> makeHttpRequest(request, imp)) - .toList(); - } - - private HttpRequest makeHttpRequest(BidRequest bidRequest, List imp) { - final BidRequest outgoingRequest = bidRequest.toBuilder().imp(imp).build(); - - return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); - } + final Imp imp = request.getImp().getFirst(); + final ExtImpResetDigital extImp; + try { + extImp = parseImpExt(imp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } - private static Imp modifyImp(Imp imp, Price bidFloorPrice) { - return imp.toBuilder() - .bidfloorcur(bidFloorPrice.getCurrency()) - .bidfloor(bidFloorPrice.getValue()) + final Imp modifiedImp = modifyImp(imp, extImp); + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(modifiedImp)) .build(); - } - private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { - final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); - return BidderUtil.isValidPrice(initialBidFloorPrice) - ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest) - : initialBidFloorPrice; + final String uri = endpointUrl + "?pid=" + HttpUtil.encodeUrl(extImp.getPlacementId()); + + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, uri, mapper)); } - private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { - final String bidFloorCur = bidFloorPrice.getCurrency(); + private ExtImpResetDigital parseImpExt(Imp imp) { try { - final BigDecimal convertedPrice = currencyConversionService - .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, DEFAULT_CURRENCY); + final ExtPrebid extPrebid = mapper.mapper() + .convertValue(imp.getExt(), EXT_TYPE_REFERENCE); - return Price.of(DEFAULT_CURRENCY, convertedPrice); - } catch (PreBidException e) { - throw new PreBidException( - "Unable to convert provided bid floor currency from %s to %s for imp `%s`" - .formatted(bidFloorCur, DEFAULT_CURRENCY, impId)); - } - } + if (extPrebid == null || extPrebid.getBidder() == null) { + throw new PreBidException("imp.ext.bidder is required"); + } - private static void populateBannerImps(List bannerImps, Price bidFloorPrice, Imp imp) { - if (imp.getBanner() != null) { - final Imp bannerImp = imp.toBuilder().video(null).xNative(null).audio(null).build(); - bannerImps.add(modifyImp(bannerImp, bidFloorPrice)); + return extPrebid.getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing resetDigitalExt from imp.ext: " + e.getMessage()); } } - private static void populateVideoImps(List videoImps, Price bidFloorPrice, Imp imp) { - if (imp.getVideo() != null) { - final Imp videoImp = imp.toBuilder().banner(null).xNative(null).audio(null).build(); - videoImps.add(modifyImp(videoImp, bidFloorPrice)); - } - } + private static Imp modifyImp(Imp imp, ExtImpResetDigital extImp) { + final Imp.ImpBuilder impBuilder = imp.toBuilder(); - private static void populateAudiImps(List audioImps, Price bidFloorPrice, Imp imp) { - if (imp.getAudio() != null) { - final Imp audioImp = imp.toBuilder().banner(null).xNative(null).video(null).build(); - audioImps.add(modifyImp(audioImp, bidFloorPrice)); + if (StringUtils.isBlank(imp.getTagid())) { + impBuilder.tagid(extImp.getPlacementId()); } + + return impBuilder.build(); } @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(bidResponse, httpCall.getRequest().getPayload())); + return extractBids(bidResponse, httpCall.getRequest().getPayload()); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List extractBids(BidResponse bidResponse, BidRequest bidRequest) { + private static Result> extractBids(BidResponse bidResponse, BidRequest bidRequest) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - if (bidResponse.getCur() != null && !StringUtils.equalsIgnoreCase(DEFAULT_CURRENCY, bidResponse.getCur())) { - throw new PreBidException("Bidder support only USD currency"); + return Result.withValues(Collections.emptyList()); } - return bidsFromResponse(bidResponse, bidRequest); - } - private static List bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid, bidRequest.getImp()), DEFAULT_CURRENCY)) - .toList(); + final String currency = StringUtils.isNotBlank(bidResponse.getCur()) + ? bidResponse.getCur() + : DEFAULT_CURRENCY; + + return bidsFromResponse(bidResponse, bidRequest, currency); } - private static BidType getBidType(Bid bid, List imps) { - final String impId = bid.getImpid(); - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getAudio() != null) { - return BidType.audio; + private static Result> bidsFromResponse(BidResponse bidResponse, + BidRequest bidRequest, + String currency) { + final List bids = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (SeatBid seatBid : bidResponse.getSeatbid()) { + if (seatBid == null || seatBid.getBid() == null) { + continue; + } + for (Bid bid : seatBid.getBid()) { + if (!BidderUtil.isValidPrice(bid.getPrice())) { + errors.add(BidderError.badServerResponse( + "price %s <= 0 filtered out".formatted(bid.getPrice()))); + continue; + } + + try { + final BidType bidType = getBidType(bid, bidRequest); + bids.add(BidderBid.of(bid, bidType, currency)); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); } } } - throw new PreBidException("Failed to find banner/video/audio impression " + impId); + + return Result.of(bids, errors); + } + + private static BidType getBidType(Bid bid, BidRequest bidRequest) { + final Integer mtype = bid.getMtype(); + if (mtype != null) { + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unsupported MType: " + mtype); + }; + } + + final Imp imp = bidRequest.getImp().getFirst(); + if (!imp.getId().equals(bid.getImpid())) { + throw new PreBidException("No matching impression found for ImpID: " + bid.getImpid()); + } + + return getMediaType(imp); + } + + private static BidType getMediaType(Imp imp) { + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + return BidType.banner; } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java index 4e4de161f66..33823866d5f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/ResetDigitalConfiguration.java @@ -2,7 +2,6 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.resetdigital.ResetDigitalBidder; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -31,16 +30,12 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps resetDigitalBidderDeps(BidderConfigurationProperties resetDigitalConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, - CurrencyConversionService currencyConversionService, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(resetDigitalConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new ResetDigitalBidder( - config.getEndpoint(), - currencyConversionService, - mapper)) + .bidderCreator(config -> new ResetDigitalBidder(config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/resources/bidder-config/resetdigital.yaml b/src/main/resources/bidder-config/resetdigital.yaml index 885916d0d8d..a5843189dc0 100644 --- a/src/main/resources/bidder-config/resetdigital.yaml +++ b/src/main/resources/bidder-config/resetdigital.yaml @@ -1,21 +1,24 @@ adapters: resetdigital: - endpoint: http://b-us-east14.resetdigital.co:9001 + endpoint: https://prebid.resetdigital.co + endpoint-compression: gzip meta-info: maintainer-email: biddersupport@resetdigital.co app-media-types: - banner - video + - native - audio site-media-types: - banner - video + - native - audio supported-vendors: vendor-id: 1162 usersync: cookie-family-name: resetdigital redirect: - url: https://sync.resetdigital.co/csync?pid=rubicon&redir={{redirect_url}} - support-cors: false - uid-macro: '$USER_ID' + url: https://sync.resetdigital.co/usersync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: true + uid-macro: '$UID' diff --git a/src/main/resources/static/bidder-params/resetdigital.json b/src/main/resources/static/bidder-params/resetdigital.json index 3710cfbc598..dc79816ce68 100644 --- a/src/main/resources/static/bidder-params/resetdigital.json +++ b/src/main/resources/static/bidder-params/resetdigital.json @@ -4,20 +4,12 @@ "description": "A schema which validates params accepted by the ResetDigital adapter", "type": "object", "properties": { - "pubId": { + "placement_id": { "type": "string", - "description": "The publisher's ID provided" - }, - "zoneId": { - "type": "string", - "description": "Zone ID" - }, - "forceBid": { - "type": "boolean", - "description": "Force bids with a test creative" + "description": "Placement ID provided by ResetDigital", + "minLength": 1 } }, - "required": [ - "pubId" - ] + "required": ["placement_id"], + "additionalProperties": false } diff --git a/src/test/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidderTest.java index dbcc555489a..7c042e4a73c 100644 --- a/src/test/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidderTest.java @@ -12,9 +12,6 @@ import com.iab.openrtb.response.SeatBid; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -22,8 +19,7 @@ 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.currency.CurrencyConversionService; -import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.response.BidType; import java.math.BigDecimal; import java.util.List; @@ -33,464 +29,346 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.AssertionsForClassTypes.tuple; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -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; -@ExtendWith(MockitoExtension.class) public class ResetDigitalBidderTest extends VertxTest { - public static final String ENDPOINT_URL = "https://test.endpoint.com"; - - @Mock - private CurrencyConversionService currencyConversionService; - + private static final String ENDPOINT_URL = "https://test.endpoint.com"; private ResetDigitalBidder target; @BeforeEach public void setUp() { - target = new ResetDigitalBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + target = new ResetDigitalBidder(ENDPOINT_URL, jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> - new ResetDigitalBidder("invalid_url", currencyConversionService, jacksonMapper)); + new ResetDigitalBidder("invalid_url", jacksonMapper)); } @Test - public void makeHttpRequestShouldReturnEmptyResponseIfAbsentAnyTypeInImp() { + public void makeHttpRequestsShouldReturnErrorWhenNoImpressions() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder.banner(null)))) - .build(); - + final BidRequest bidRequest = BidRequest.builder().imp(List.of()).build(); // when final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()).hasSize(0); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("ResetDigital adapter supports only one impression per request"); } @Test - public void makeHttpRequestShouldReturnEmptyResponseIfxNativeImpTypePresent() { + public void makeHttpRequestsShouldReturnErrorWhenMultipleImpressions() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder.banner(null) - .xNative(Native.builder().build())))) + .imp(List.of(givenImp(identity()), givenImp(identity()))) .build(); - // when final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()).hasSize(0); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("ResetDigital adapter supports only one impression per request"); } @Test - public void makeHttpRequestShouldReturnSeparateResponseWithBannerAndVideoAndAudioImp() { + public void makeHttpRequestsShouldReturnErrorWhenImpExtIsInvalid() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder - .audio(Audio.builder().build()) - .video(Video.builder().build())))) + .imp(singletonList(Imp.builder().id("123").build())) .build(); - // when final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()).hasSize(3); - assertThat(result.getValue().get(0)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getBanner) - .isNotNull(); - - assertThat(result.getValue().get(1)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getVideo) - .isNotNull(); - - assertThat(result.getValue().get(2)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getAudio) - .isNotNull(); + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).contains("imp.ext.bidder is required"); + }); } @Test - public void makeHttpRequestShouldReturnSeparateResponseWithBannerAndVideoImp() { + public void makeHttpRequestsShouldCreateCorrectUrl() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder - .video(Video.builder().build())))) - .build(); - + final BidRequest bidRequest = givenBidRequest(identity()); // when final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()).hasSize(2); - assertThat(result.getValue().get(0)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getBanner) - .isNotNull(); - - assertThat(result.getValue().get(1)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getVideo) - .isNotNull(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?pid=placementId123"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void makeHttpRequestShouldReturnSeparateResponseWithBannerAndAudioImp() { + public void makeHttpRequestsShouldSetTagIdFromPlacementIdWhenEmpty() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder - .audio(Audio.builder().build())))) - .build(); - + final BidRequest bidRequest = givenBidRequest(identity()); // when final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()).hasSize(2); - assertThat(result.getValue().get(0)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getBanner) - .isNotNull(); - - assertThat(result.getValue().get(1)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getAudio) - .isNotNull(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getPayload().getImp().getFirst().getTagid()) + .isEqualTo("placementId123"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void makeHttpRequestShouldReturnSeparateResponseWithVideoAndAudioImp() { + public void makeHttpRequestsShouldNotOverrideExistingTagId() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder - .banner(null) - .video(Video.builder().build()) - .audio(Audio.builder().build())))) - .build(); - + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.tagid("existingTagId")); // when final Result>> result = target.makeHttpRequests(bidRequest); - // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()).hasSize(2); - assertThat(result.getValue().get(0)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getVideo) - .isNotNull(); - - assertThat(result.getValue().get(1)) - .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getImp) - .extracting(a -> a.getFirst()) - .extracting(Imp::getAudio) - .isNotNull(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getPayload().getImp().getFirst().getTagid()) + .isEqualTo("existingTagId"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void makeHttpRequestShouldReturnResponseOnlyWithBannerImp() { + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(identity()))) - .build(); - + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), "invalid"); // when - final Result>> result = target.makeHttpRequests(bidRequest); - + final Result> result = target.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .allSatisfy(imp -> { - assertThat(imp.getBanner()).isNotNull(); - assertThat(imp.getVideo()).isNull(); - assertThat(imp.getAudio()).isNull(); + 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 makeHttpRequestShouldReturnResponseOnlyWithVideoImp() { + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder.banner(null) - .video(Video.builder().build())))) - .build(); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString(null)); // when - final Result>> result = target.makeHttpRequests(bidRequest); - + final Result> result = target.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .allSatisfy(imp -> { - assertThat(imp.getBanner()).isNull(); - assertThat(imp.getVideo()).isNotNull(); - assertThat(imp.getAudio()).isNull(); - }); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); } @Test - public void makeHttpRequestShouldReturnResponseOnlyWithAudioImp() { + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(givenImp(impBuilder -> impBuilder.banner(null) - .audio(Audio.builder().build())))) - .build(); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString(BidResponse.builder().build())); // when - final Result>> result = target.makeHttpRequests(bidRequest); - + final Result> result = target.makeBids(httpCall, null); // then - assertThat(result.getErrors()).hasSize(0); - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .allSatisfy(imp -> { - assertThat(imp.getBanner()).isNull(); - assertThat(imp.getVideo()).isNull(); - assertThat(imp.getAudio()).isNotNull(); - }); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); } @Test - public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + public void makeBidsShouldReturnBannerBidByMType() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, "invalid"); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(1)))); // 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(); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(1) + .price(BigDecimal.ONE).build(), BidType.banner, "USD")); } @Test - public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBidderCurrency() { + public void makeBidsShouldReturnVideoBidByMType() throws JsonProcessingException { // given - given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) - .willReturn(BigDecimal.TEN); - - final BidRequest bidRequest = givenBidRequest( - impBuilder -> impBuilder.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(2)))); // when - final Result>> result = target.makeHttpRequests(bidRequest); - + final Result> result = target.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getBidfloor, Imp::getBidfloorcur) - .containsOnly(tuple(BigDecimal.TEN, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(2) + .price(BigDecimal.ONE).build(), BidType.video, "USD")); } @Test - public void makeHttpRequestsShouldReturnErrorMessageOnFailedCurrencyConversion() { + public void makeBidsShouldReturnNativeBidByMType() throws JsonProcessingException { // given - given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) - .willThrow(PreBidException.class); - - final BidRequest bidRequest = givenBidRequest( - impCustomizer -> impCustomizer.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(4)))); // when - final Result>> result = target.makeHttpRequests(bidRequest); - + final Result> result = target.makeBids(httpCall, null); // then - assertThat(result.getErrors()).allSatisfy(bidderError -> { - assertThat(bidderError.getType()) - .isEqualTo(BidderError.Type.bad_input); - assertThat(bidderError.getMessage()) - .isEqualTo("Unable to convert provided bid floor currency from EUR to USD for imp `123`"); - }); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4) + .price(BigDecimal.ONE).build(), BidType.xNative, "USD")); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + public void makeBidsShouldReturnAudioBidByMType() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(3)))); // when final Result> result = target.makeBids(httpCall, null); - // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(3) + .price(BigDecimal.ONE).build(), BidType.audio, "USD")); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidFromImpWhenMTypeIsNull() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(BidResponse.builder().build())); - + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when final Result> result = target.makeBids(httpCall, null); - // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123") + .price(BigDecimal.ONE).build(), BidType.banner, "USD")); } @Test - public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBidFromImpWhenMTypeIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) - .build(), - mapper.writeValueAsString( + givenBidRequest(impBuilder -> impBuilder.banner(null).video(Video.builder().build())), + jacksonMapper.mapper().writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); - // when final Result> result = target.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") + .price(BigDecimal.ONE).build(), BidType.video, "USD")); } @Test - public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnAudioBidFromImpWhenMTypeIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").video(Video.builder().build()).build())) - .build(), - mapper.writeValueAsString( + givenBidRequest(impBuilder -> impBuilder.banner(null).audio(Audio.builder().build())), + jacksonMapper.mapper().writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); - // when final Result> result = target.makeBids(httpCall, null); - // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123") + .price(BigDecimal.ONE).build(), BidType.audio, "USD")); } @Test - public void makeBidsShouldReturnAudioBidIfAudioIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnNativeBidFromImpWhenMTypeIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").audio(Audio.builder().build()).build())) - .build(), - mapper.writeValueAsString( + givenBidRequest(impBuilder -> impBuilder.banner(null).xNative(Native.builder().build())), + jacksonMapper.mapper().writeValueAsString( givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); - // when final Result> result = target.makeBids(httpCall, null); - // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); + .containsExactly(BidderBid.of(Bid.builder().impid("123") + .price(BigDecimal.ONE).build(), BidType.xNative, "USD")); } @Test - public void makeBidsShouldReturnErrorBidIfBidTypeIsAbsentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldFilterOutZeroPriceBids() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); - + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").price(BigDecimal.ZERO)))); // when final Result> result = target.makeBids(httpCall, null); - // then + assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .extracting(BidderError::getMessage) - .containsExactly("Failed to find banner/video/audio impression 123"); - assertThat(result.getValue()).isEmpty(); + .containsExactly("price 0 <= 0 filtered out"); } @Test - public void makeBidsShouldReturnErrorIfBidCurIsNotUsd() throws JsonProcessingException { + public void makeBidsShouldReturnErrorWhenMTypeIsUnsupported() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").build())) - .build(), - mapper.writeValueAsString(givenBidResponse(identity()).toBuilder().cur("EUR").build())); - + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(99)))); // when final Result> result = target.makeBids(httpCall, null); - // then + assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .extracting(BidderError::getMessage) - .containsExactly("Bidder support only USD currency"); - assertThat(result.getValue()).isEmpty(); + .containsExactly("Unsupported MType: 99"); } - private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, - UnaryOperator impCustomizer) { - - return bidRequestCustomizer.apply(BidRequest.builder() - .imp(singletonList(givenImp(impCustomizer)))) - .build(); + @Test + public void makeBidsShouldUseCurrencyFromBidResponse() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + jacksonMapper.mapper().writeValueAsString( + givenBidResponse(identity()).toBuilder().cur("EUR").build())); + // when + final Result> result = target.makeBids(httpCall, null); + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(BidderBid::getBidCurrency) + .containsExactly("EUR"); } - private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { - return givenBidRequest(identity(), impCustomizer); + private BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); } - private static Imp givenImp(UnaryOperator impCustomizer) { + private Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id("123") - .banner(Banner.builder().w(23).h(25).build())) + .banner(Banner.builder().w(300).h(250).build()) + .ext(jacksonMapper.mapper().createObjectNode() + .set("bidder", jacksonMapper.mapper().createObjectNode() + .put("placement_id", "placementId123")))) .build(); } private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { return BidResponse.builder() - .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder() + .impid("123") + .price(BigDecimal.ONE)).build())) .build())) .cur("USD") .build(); diff --git a/src/test/java/org/prebid/server/it/ResetDigitalTest.java b/src/test/java/org/prebid/server/it/ResetDigitalTest.java index 48138dfccfe..5264c29fc7e 100644 --- a/src/test/java/org/prebid/server/it/ResetDigitalTest.java +++ b/src/test/java/org/prebid/server/it/ResetDigitalTest.java @@ -8,6 +8,7 @@ import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; @@ -19,6 +20,7 @@ public class ResetDigitalTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromResetDigital() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/resetdigital-exchange")) + .withQueryParam("pid", equalTo("publisherTestID")) .withRequestBody(equalToJson(jsonFrom("openrtb2/resetdigital/test-resetdigital-bid-request.json"))) .willReturn(aResponse().withBody( jsonFrom("openrtb2/resetdigital/test-resetdigital-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-request.json b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-request.json index 9ee75999128..99e50554443 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-request.json @@ -9,8 +9,7 @@ }, "ext": { "resetdigital": { - "pubId": "lb.ads", - "zoneId": "publisherTestID" + "placement_id": "publisherTestID" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-resetdigital-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-resetdigital-bid-request.json index 43d9d50841b..79219b2748f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-resetdigital-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-resetdigital-bid-request.json @@ -8,11 +8,11 @@ "w": 300, "h": 250 }, + "tagid": "publisherTestID", "ext": { "tid": "${json-unit.any-string}", "bidder": { - "pubId": "lb.ads", - "zoneId": "publisherTestID" + "placement_id": "publisherTestID" } } }