From d64feeb58b77e83f5c95586d0ca88dc7a52f4114 Mon Sep 17 00:00:00 2001 From: pkaczmarek Date: Thu, 12 Feb 2026 14:20:31 +0100 Subject: [PATCH 1/2] ResetDigital: Switch to OpenRTB --- .../resetdigital/ResetDigitalBidder.java | 207 ++++---- .../bidder/ResetDigitalConfiguration.java | 7 +- .../resources/bidder-config/resetdigital.yaml | 11 +- .../static/bidder-params/resetdigital.json | 18 +- .../resetdigital/ResetDigitalBidderTest.java | 444 +++++++----------- .../prebid/server/it/ResetDigitalTest.java | 2 + .../test-auction-resetdigital-request.json | 3 +- .../test-resetdigital-bid-request.json | 4 +- 8 files changed, 280 insertions(+), 416 deletions(-) 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" } } } From 1526a3506232a0b02d5aced9b4184e3096ec2d15 Mon Sep 17 00:00:00 2001 From: pkaczmarek Date: Tue, 24 Feb 2026 16:20:26 +0100 Subject: [PATCH 2/2] ResetDigital: Switch to OpenRTB --- .../resetdigital/ResetDigitalBidder.java | 103 ++++++++---------- .../resetdigital/ResetDigitalBidderTest.java | 26 ++++- 2 files changed, 72 insertions(+), 57 deletions(-) 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 d0ab8651bc3..683695c4c7d 100644 --- a/src/main/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidder.java @@ -6,6 +6,7 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; @@ -27,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; public class ResetDigitalBidder implements Bidder { @@ -45,7 +47,7 @@ public ResetDigitalBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - if (CollectionUtils.isEmpty(request.getImp()) || request.getImp().size() != 1) { + if (request.getImp().size() != 1) { return Result.withError(BidderError.badInput( "ResetDigital adapter supports only one impression per request")); } @@ -64,107 +66,98 @@ public Result>> makeHttpRequests(BidRequest request .build(); final String uri = endpointUrl + "?pid=" + HttpUtil.encodeUrl(extImp.getPlacementId()); + final MultiMap headers = HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); - return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, uri, mapper)); + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, headers, uri, mapper)); } private ExtImpResetDigital parseImpExt(Imp imp) { try { - final ExtPrebid extPrebid = mapper.mapper() - .convertValue(imp.getExt(), EXT_TYPE_REFERENCE); - - if (extPrebid == null || extPrebid.getBidder() == null) { - throw new PreBidException("imp.ext.bidder is required"); - } - - return extPrebid.getBidder(); + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException("Error parsing resetDigitalExt from imp.ext: " + e.getMessage()); } } private static Imp modifyImp(Imp imp, ExtImpResetDigital extImp) { - final Imp.ImpBuilder impBuilder = imp.toBuilder(); - - if (StringUtils.isBlank(imp.getTagid())) { - impBuilder.tagid(extImp.getPlacementId()); - } - - return impBuilder.build(); + return StringUtils.isBlank(imp.getTagid()) + ? imp.toBuilder().tagid(extImp.getPlacementId()).build() + : imp; } @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { + final List errors = new ArrayList<>(); final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return extractBids(bidResponse, httpCall.getRequest().getPayload()); + return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload(), errors), errors); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static Result> extractBids(BidResponse bidResponse, BidRequest bidRequest) { + private static List extractBids(BidResponse bidResponse, + BidRequest bidRequest, + List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Result.withValues(Collections.emptyList()); + return Collections.emptyList(); } + final Imp imp = bidRequest.getImp().getFirst(); final String currency = StringUtils.isNotBlank(bidResponse.getCur()) ? bidResponse.getCur() - : DEFAULT_CURRENCY; - - return bidsFromResponse(bidResponse, bidRequest, currency); - } - - private static Result> bidsFromResponse(BidResponse bidResponse, - BidRequest bidRequest, - String currency) { - final List bids = new ArrayList<>(); - final List errors = new ArrayList<>(); + : bidRequest.getCur().stream().findFirst().orElse(DEFAULT_CURRENCY); + final List bidderBids = new ArrayList<>(); for (SeatBid seatBid : bidResponse.getSeatbid()) { - if (seatBid == null || seatBid.getBid() == null) { + if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) { continue; } - for (Bid bid : seatBid.getBid()) { - if (!BidderUtil.isValidPrice(bid.getPrice())) { - errors.add(BidderError.badServerResponse( - "price %s <= 0 filtered out".formatted(bid.getPrice()))); - continue; - } + for (Bid bid : seatBid.getBid()) { try { - final BidType bidType = getBidType(bid, bidRequest); - bids.add(BidderBid.of(bid, bidType, currency)); + bidderBids.add(makeBidderBid(bid, seatBid.getSeat(), currency, imp)); } catch (PreBidException e) { errors.add(BidderError.badServerResponse(e.getMessage())); } } } - return Result.of(bids, errors); + return bidderBids; } - 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); - }; + private static BidderBid makeBidderBid(Bid bid, String seat, String currency, Imp imp) { + if (!BidderUtil.isValidPrice(bid.getPrice())) { + throw new PreBidException("price %s <= 0 filtered out".formatted(bid.getPrice())); } - final Imp imp = bidRequest.getImp().getFirst(); + final BidType bidType = Optional.ofNullable(getBidType(bid)) + .orElseGet(() -> getBidType(bid, imp)); + + return StringUtils.isNotBlank(seat) + ? BidderBid.of(bid, bidType, seat, currency) + : BidderBid.of(bid, bidType, currency); + } + + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null -> null; + default -> throw new PreBidException("Unsupported MType: " + mtype); + }; + } + + private static BidType getBidType(Bid bid, Imp imp) { 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) { 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 7c042e4a73c..71b8c798266 100644 --- a/src/test/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/resetdigital/ResetDigitalBidderTest.java @@ -10,6 +10,7 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; @@ -20,15 +21,18 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; import java.util.List; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Collections.singletonList; 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.Assertions.tuple; public class ResetDigitalBidderTest extends VertxTest { @@ -78,7 +82,10 @@ public void makeHttpRequestsShouldReturnErrorWhenMultipleImpressions() { public void makeHttpRequestsShouldReturnErrorWhenImpExtIsInvalid() { // given final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder().id("123").build())) + .imp(singletonList(Imp.builder() + .id("123") + .ext(jacksonMapper.mapper().createObjectNode().put("bidder", "invalid")) + .build())) .build(); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -87,7 +94,7 @@ public void makeHttpRequestsShouldReturnErrorWhenImpExtIsInvalid() { assertThat(result.getErrors()).hasSize(1) .allSatisfy(error -> { assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); - assertThat(error.getMessage()).contains("imp.ext.bidder is required"); + assertThat(error.getMessage()).startsWith("Error parsing resetDigitalExt from imp.ext"); }); } @@ -104,6 +111,21 @@ public void makeHttpRequestsShouldCreateCorrectUrl() { assertThat(result.getErrors()).isEmpty(); } + @Test + public void makeHttpRequestsShouldContainXOpenRtbVersionHeader() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getHeaders) + .flatExtracting(MultiMap::entries) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnlyOnce(tuple(HttpUtil.X_OPENRTB_VERSION_HEADER.toString(), "2.5")); + assertThat(result.getErrors()).isEmpty(); + } + @Test public void makeHttpRequestsShouldSetTagIdFromPlacementIdWhenEmpty() { // given