From 356e7ae399c3267b6377237263d167577037feff Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:44:38 -0400 Subject: [PATCH 001/170] Tests: Fix flaky floors test (#3354) --- .../tests/pricefloors/PriceFloorsEnforcementSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy index 2ae431c1530..4d47947670a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy @@ -869,7 +869,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS cache rules" - cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) + cacheFloorsProviderRules(bidRequest, floorValue, pbsService) and: "Bid response with 2 bids: bid.price = floorValue, dealBid.price < floorValue" def dealBidPrice = floorValue - 0.1 From 190ae6e30607d11c7d9bfe7e3456aa3fd55f5acd Mon Sep 17 00:00:00 2001 From: gg-natalia <148577437+gg-natalia@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:47:13 -0300 Subject: [PATCH 002/170] Gumgum: Remove video validations (#3357) --- .../server/bidder/gumgum/GumgumBidder.java | 13 ------- .../bidder/gumgum/GumgumBidderTest.java | 38 ------------------- 2 files changed, 51 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java index 783caad5d1a..cecb9e3c473 100644 --- a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java +++ b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java @@ -135,7 +135,6 @@ private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { final Video video = imp.getVideo(); if (video != null) { - validateVideoParams(video); final String irisId = extImp.getIrisId(); if (StringUtils.isNotEmpty(irisId)) { final Video resolvedVideo = resolveVideo(video, irisId); @@ -174,18 +173,6 @@ private static ExtImpGumgumBanner resolveBannerExt(List formats, Long sl .orElseGet(() -> ExtImpGumgumBanner.of(slot, 0, 0)); } - private void validateVideoParams(Video video) { - if (anyOfNull( - video.getW(), - video.getH(), - video.getMinduration(), - video.getMaxduration(), - video.getPlacement(), - video.getLinearity())) { - throw new PreBidException("Invalid or missing video field(s)"); - } - } - private Video resolveVideo(Video video, String irisId) { final ObjectNode videoExt = mapper.mapper().valueToTree(ExtImpGumgumVideo.of(irisId)); return video.toBuilder().ext(videoExt).build(); diff --git a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java index af166d87107..2595b6d903a 100644 --- a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java @@ -72,44 +72,6 @@ public void makeHttpRequestsShouldReturnErrorsIfImpExtCouldNotBeParsed() { assertThat(result.getValue()).isEmpty(); } - @Test - public void makeHttpRequestsShouldReturnErrorIfNoValidImpressions() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList( - givenImp(impBuilder -> impBuilder.video(Video.builder().build())))) - .build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(2) - .contains(BidderError.badInput("No valid impressions")); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfVideoFieldsAreNotValid() { - // given - final BidRequest bidRequest = BidRequest.builder() - .imp(singletonList(Imp.builder() - .video(Video.builder().w(0).build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpGumgum.of("zone", BigInteger.TEN, "irisId", null, null)))) - .build())) - .build(); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()) - .containsExactlyInAnyOrder(BidderError.badInput("Invalid or missing video field(s)"), - BidderError.badInput("No valid impressions")); - assertThat(result.getValue()).isEmpty(); - } - @Test public void makeHttpRequestsShouldModifyVideoExtOfIrisIdIsPresent() { // given From 075fd9087bf27479adb79a6882c61628c90d053d Mon Sep 17 00:00:00 2001 From: bretg Date: Wed, 7 Aug 2024 06:57:58 -0400 Subject: [PATCH 003/170] Rubicon: Add multiformat support (#3347) --- src/main/resources/bidder-config/rubicon.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/bidder-config/rubicon.yaml b/src/main/resources/bidder-config/rubicon.yaml index 7f886ab5634..38a3e2d24aa 100644 --- a/src/main/resources/bidder-config/rubicon.yaml +++ b/src/main/resources/bidder-config/rubicon.yaml @@ -11,6 +11,8 @@ adapters: aliases: magnite: enabled: false + ortb: + multiformat-supported: true meta-info: maintainer-email: header-bidding@rubiconproject.com app-media-types: From b93c5ce9eeeb442647d1c2db4fcbab68fd23e6c7 Mon Sep 17 00:00:00 2001 From: Laurentiu Badea Date: Fri, 9 Aug 2024 05:17:47 -0700 Subject: [PATCH 004/170] OpenX: Populate BidderBid.videoInfo for targeting (#3364) --- .../server/bidder/openx/OpenxBidder.java | 19 +++++++++- .../server/bidder/openx/OpenxBidderTest.java | 37 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java index fec1f7d2ddf..2ef79d3bfd4 100644 --- a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java +++ b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java @@ -29,6 +29,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.openx.ExtImpOpenx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -253,10 +254,26 @@ private static List bidsFromResponse(BidRequest bidRequest, OpenxBidR .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid, impIdToBidType), bidCurrency)) + .map(bid -> toBidderBid(bid, impIdToBidType, bidCurrency)) .toList(); } + private static BidderBid toBidderBid(Bid bid, Map impIdToBidType, String bidCurrency) { + final BidType bidType = getBidType(bid, impIdToBidType); + final ExtBidPrebidVideo videoInfo = bidType == BidType.video ? getVideoInfo(bid) : null; + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(bidCurrency) + .videoInfo(videoInfo) + .build(); + } + + private static ExtBidPrebidVideo getVideoInfo(Bid bid) { + final String primaryCategory = CollectionUtils.isEmpty(bid.getCat()) ? null : bid.getCat().getFirst(); + return ExtBidPrebidVideo.of(bid.getDur(), primaryCategory); + } + private static Map impIdToBidType(BidRequest bidRequest) { return bidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, imp -> imp.getBanner() != null ? BidType.banner : BidType.video)); diff --git a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java index 315f0499cee..1f218984f91 100644 --- a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java @@ -34,6 +34,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.openx.ExtImpOpenx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.math.BigDecimal; @@ -522,6 +523,42 @@ public void makeBidsShouldReturnResultWithExpectedFields() throws JsonProcessing .build()); } + @Test + public void makeBidsShouldReturnVideoInfoWhenAvailable() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(Bid.builder() + .w(200) + .h(150) + .price(BigDecimal.ONE) + .impid("impId1") + .dealid("dealid") + .adm("
This is an Ad
") + .dur(30) + .cat(singletonList("category1")) + .build())) + .build())) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .build())) + .build(); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1) + .extracting(BidderBid::getVideoInfo) + .containsExactly(ExtBidPrebidVideo.of(30, "category1")); + } + @Test public void makeBidsShouldReturnFledgeConfigEvenIfNoBids() throws JsonProcessingException { // given From 73b9b172d5072b6e73ee3f25608e7a50801ae45f Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 9 Aug 2024 15:04:28 +0200 Subject: [PATCH 005/170] Criteo: Support Fledge (#3344) --- .../bidder/criteo/CriteoBidResponse.java | 27 ++++ .../server/bidder/criteo/CriteoBidder.java | 38 ++++- .../bidder/criteo/CriteoExtBidResponse.java | 11 ++ .../criteo/CriteoIgiExtBidResponse.java | 13 ++ .../criteo/CriteoIgsIgiExtBidResponse.java | 10 ++ .../bidder/criteo/CriteoBidderTest.java | 135 ++++++++++++------ 6 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java create mode 100644 src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java create mode 100644 src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java create mode 100644 src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java new file mode 100644 index 00000000000..d6f43ef4c86 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidResponse.java @@ -0,0 +1,27 @@ +package org.prebid.server.bidder.criteo; + +import com.iab.openrtb.response.SeatBid; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder(toBuilder = true) +@Value +public class CriteoBidResponse { + + String id; + + List seatbid; + + String bidid; + + String cur; + + String customdata; + + Integer nbr; + + CriteoExtBidResponse ext; + +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java index db9194ee215..064bac48c48 100644 --- a/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoBidder.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; @@ -19,6 +19,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -30,6 +31,7 @@ public class CriteoBidder implements Bidder { + private static final String BIDDER_NAME = "criteo"; private final String endpointUrl; private final JacksonMapper mapper; @@ -44,16 +46,24 @@ public Result>> makeHttpRequests(BidRequest bidRequ } @Override + @Deprecated(forRemoval = true) public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + return Result.withError(BidderError.generic("Deprecated adapter method invoked")); + } + + @Override + public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBidsFromResponse(bidResponse)); + final CriteoBidResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), + CriteoBidResponse.class); + return CompositeBidderResponse.withBids(extractBids(bidResponse), extractFledge(bidResponse)); } catch (DecodeException | PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBidsFromResponse(BidResponse bidResponse) { + private List extractBids(CriteoBidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -94,4 +104,22 @@ private ObjectNode makeExt(String networkName) { .meta(ExtBidPrebidMeta.builder().networkName(networkName).build()) .build()); } + + private static List extractFledge(CriteoBidResponse bidResponse) { + final List fledgeConfigs = Optional.ofNullable(bidResponse) + .map(CriteoBidResponse::getExt) + .map(CriteoExtBidResponse::getIgi) + .filter(CollectionUtils::isNotEmpty) + .orElse(Collections.emptyList()) + .stream() + .filter(igi -> CollectionUtils.isNotEmpty(igi.getIgs()) && igi.getIgs().getFirst() != null) + .map(igi -> FledgeAuctionConfig.builder() + .impId(igi.getImpId()) + .bidder(BIDDER_NAME) + .config(igi.getIgs().getFirst().getConfig()) + .build()) + .toList(); + + return CollectionUtils.isEmpty(fledgeConfigs) ? null : fledgeConfigs; + } } diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java new file mode 100644 index 00000000000..8f332b2f3bd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoExtBidResponse { + + List igi; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java new file mode 100644 index 00000000000..6bdac80ad2f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgiExtBidResponse.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoIgiExtBidResponse { + + String impId; + + List igs; +} diff --git a/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java new file mode 100644 index 00000000000..b34d76e0646 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoIgsIgiExtBidResponse.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.criteo; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class CriteoIgsIgiExtBidResponse { + + ObjectNode config; +} diff --git a/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java b/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java index af44957b2b6..09f2f113fb1 100644 --- a/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/criteo/CriteoBidderTest.java @@ -15,10 +15,12 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.CompositeBidderResponse; 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.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.util.List; import java.util.Set; @@ -73,12 +75,12 @@ public void makeHttpRequestsShouldEncodePassedBidRequest() { } @Test - public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + public void makeBidderResponseShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given final BidderCall httpCall = givenHttpCall("invalid"); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).hasSize(1) @@ -86,127 +88,126 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); }); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + public void makeBidderResponseShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsEmpty() throws JsonProcessingException { + public void makeBidderResponseShouldReturnEmptyListIfBidResponseSeatBidIsEmpty() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( mapper.writeValueAsString(BidResponse.builder().seatbid(emptyList()).build())); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnErrorWhenBidExtPrebidTypeIsNotPresent() throws JsonProcessingException { + public void makeBidderResponseShouldReturnErrorWhenBidExtPrebidTypeIsNotPresent() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.impid("123")))); + givenBidResponse(bid -> bid.impid("123"))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()) .containsExactly(BidderError.badServerResponse("Missing ext.prebid.type in bid for impression : 123.")); - assertThat(result.getValue()).isEmpty(); + assertThat(result.getBids()).isEmpty(); } @Test - public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + public void makeBidderResponseShouldReturnBannerBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner))))); + givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getType) .containsExactly(BidType.banner); } @Test - public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + public void makeBidderResponseShouldReturnVideoBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.ext(givenBidExt(BidType.video))))); + givenBidResponse(bid -> bid.ext(givenBidExt(BidType.video)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getType) .containsExactly(BidType.video); } @Test - public void makeBidsShouldReturnBidWithCurFromResponse() throws JsonProcessingException { + public void makeBidderResponseShouldReturnBidWithCurFromResponse() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner))))); + givenBidResponse(bid -> bid.ext(givenBidExt(BidType.banner)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getBidCurrency) .containsExactly("CUR"); } @Test - public void makeBidsShouldReturnBidWithNetworkNameFromExtPrebid() throws JsonProcessingException { + public void makeBidderResponseShouldReturnBidWithNetworkNameFromExtPrebid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse( - bid -> bid + givenBidResponse(bid -> bid .impid("123") - .ext(givenBidExtWithNetwork("anyNetworkName"))))); + .ext(givenBidExtWithNetwork("anyNetworkName")))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getBid) .extracting(Bid::getExt) .extracting(ext -> ext.get("meta")) @@ -214,32 +215,82 @@ public void makeBidsShouldReturnBidWithNetworkNameFromExtPrebid() throws JsonPro } @Test - public void makeBidsShouldReturnEmptyNetworkNameWhenBidExtPrebidNotContainNetworkName() + public void makeBidderResponseShouldReturnEmptyNetworkNameWhenBidExtPrebidNotContainNetworkName() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - mapper.writeValueAsString(givenBidResponse( - bid -> bid.ext(givenBidExtWithNetwork(null))))); + givenBidResponse(bid -> bid.ext(givenBidExtWithNetwork(null)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .extracting(BidderBid::getBid) .extracting(Bid::getExt) .extracting(ext -> ext.get("meta")) .doesNotContain(mapper.createObjectNode().put("networkName", "anyNetworkName")); } - private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { - return BidResponse.builder() + @Test + public void makeBidderResponseShouldReturnFledgeConfigs() throws JsonProcessingException { + // given + final CriteoBidResponse bidResponseWithFledge = CriteoBidResponse.builder() + .ext(CriteoExtBidResponse.of(List.of( + CriteoIgiExtBidResponse.of("imp_id1", List.of( + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty1", "value1")), + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty2", "value2")))), + CriteoIgiExtBidResponse.of("imp_id2", List.of( + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty3", "value3")), + CriteoIgsIgiExtBidResponse.of( + mapper.createObjectNode().put("proterty4", "value4"))))))) + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponseWithFledge)); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getFledgeAuctionConfigs()) + .containsExactlyInAnyOrder( + FledgeAuctionConfig.builder() + .bidder("criteo") + .impId("imp_id1") + .config(mapper.createObjectNode().put("proterty1", "value1")) + .build(), + FledgeAuctionConfig.builder() + .bidder("criteo") + .impId("imp_id2") + .config(mapper.createObjectNode().put("proterty3", "value3")) + .build()); + } + + @Test + public void makeBidsShouldFail() throws JsonProcessingException { + //given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.generic("Deprecated adapter method invoked")); + } + + private static String givenBidResponse(UnaryOperator bidCustomizer) + throws JsonProcessingException { + + return mapper.writeValueAsString(BidResponse.builder() .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .cur("CUR") - .build(); + .build()); } private static BidderCall givenHttpCall(String body) { From 68cc8421e6ede87de3b269931f243bcddf3ee14c Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Fri, 9 Aug 2024 15:33:41 +0200 Subject: [PATCH 006/170] QT: Add adapter (#3355) --- .../org/prebid/server/bidder/qt/QtBidder.java | 137 ++++++++ .../bidder/qt/proto/QtImpExtBidder.java | 18 + .../proto/openrtb/ext/request/ExtImpQt.java | 14 + .../spring/config/bidder/QtConfiguration.java | 41 +++ src/main/resources/bidder-config/qt.yaml | 21 ++ .../resources/static/bidder-params/qt.json | 30 ++ .../prebid/server/bidder/qt/QtBidderTest.java | 326 ++++++++++++++++++ .../bidder/vidazoo/VidazooBidderTest.java | 2 +- .../java/org/prebid/server/it/QtTest.java | 33 ++ .../openrtb2/qt/test-auction-qt-request.json | 26 ++ .../openrtb2/qt/test-auction-qt-response.json | 37 ++ .../it/openrtb2/qt/test-qt-bid-request.json | 59 ++++ .../it/openrtb2/qt/test-qt-bid-response.json | 20 ++ .../server/it/test-application.properties | 2 + 14 files changed, 765 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/prebid/server/bidder/qt/QtBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java create mode 100644 src/main/resources/bidder-config/qt.yaml create mode 100644 src/main/resources/static/bidder-params/qt.json create mode 100644 src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/QtTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/qt/QtBidder.java b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java new file mode 100644 index 00000000000..a53ca985d6a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java @@ -0,0 +1,137 @@ +package org.prebid.server.bidder.qt; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder; +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.ExtImpQt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class QtBidder implements Bidder { + + private static final TypeReference> QT_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public QtBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpQt extImpQt; + try { + extImpQt = parseImpExt(imp); + outgoingRequests.add(createSingleRequest(modifyImp(imp, extImpQt), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(outgoingRequests, errors); + } + + private ExtImpQt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), QT_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpQt extImpQt) { + final QtImpExtBidder qtImpExtBidder = getImpExtQtWithType(extImpQt); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(qtImpExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private QtImpExtBidder getImpExtQtWithType(ExtImpQt extImpQt) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpQt.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpQt.getEndpointId()); + + return QtImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpQt.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpQt.getEndpointId() : null) + .build(); + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java b/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java new file mode 100644 index 00000000000..4f9abd708c4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/qt/proto/QtImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.qt.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class QtImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java new file mode 100644 index 00000000000..084e236a063 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpQt { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java new file mode 100644 index 00000000000..0ccfe750403 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/QtConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.qt.QtBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/qt.yaml", factory = YamlPropertySourceFactory.class) +public class QtConfiguration { + + private static final String BIDDER_NAME = "qt"; + + @Bean("qtConfigurationProperties") + @ConfigurationProperties("adapters.qt") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps qtBidderDeps(BidderConfigurationProperties qtConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(qtConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new QtBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/qt.yaml b/src/main/resources/bidder-config/qt.yaml new file mode 100644 index 00000000000..67e006405d3 --- /dev/null +++ b/src/main/resources/bidder-config/qt.yaml @@ -0,0 +1,21 @@ +adapters: + qt: + endpoint: https://endpoint1.qt.io/pserver + meta-info: + maintainer-email: qtssp-support@qt.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: qt + redirect: + support-cors: false + url: https://cs.qt.io/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/static/bidder-params/qt.json b/src/main/resources/static/bidder-params/qt.json new file mode 100644 index 00000000000..ef7eb77a9ac --- /dev/null +++ b/src/main/resources/static/bidder-params/qt.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "QT Adapter Params", + "description": "A schema which validates params accepted by the QT adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java b/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java new file mode 100644 index 00000000000..660049e507a --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java @@ -0,0 +1,326 @@ +package org.prebid.server.bidder.qt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder; +import org.prebid.server.bidder.qt.proto.QtImpExtBidder.QtImpExtBidderBuilder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtImpQt; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class QtBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final QtBidder target = new QtBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new QtBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final Imp givenInvalidImp = givenImp(imp -> imp + .id("impIdCorrupted") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final Imp givenValidImp = givenImp(identity()); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(givenInvalidImp, givenValidImp)) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("123"); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(identity()), givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpQt.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtQtBidder(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpQt.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtQtBidder(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpQt.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExtQtBidder(UnaryOperator impExtQt) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExtQt.apply(QtImpExtBidder.builder()).build(), + JsonNode.class)); + } +} diff --git a/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java b/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java index 00c1c100a9c..f5446b4c647 100644 --- a/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/vidazoo/VidazooBidderTest.java @@ -101,7 +101,7 @@ public void makeHttpRequestsShouldReturnExpectedHeaders() { public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { // given final Imp givenInvalidImp = givenImp(imp -> imp - .id("impId") + .id("impIdCorrupted") .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); final Imp givenValidImp = givenImp(identity()); diff --git a/src/test/java/org/prebid/server/it/QtTest.java b/src/test/java/org/prebid/server/it/QtTest.java new file mode 100644 index 00000000000..eff861a5434 --- /dev/null +++ b/src/test/java/org/prebid/server/it/QtTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class QtTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromQt() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/qt-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/qt/test-qt-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/qt/test-qt-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/qt/test-auction-qt-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/qt/test-auction-qt-response.json", response, + singletonList("qt")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json new file mode 100644 index 00000000000..e75dfb6e4e0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "qt": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json new file mode 100644 index 00000000000..4ebd8ee119a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "qt", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "qt": "{{ qt.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-qt-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} 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 2a06064defa..9e313b9d896 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -336,6 +336,8 @@ adapters.pulsepoint.enabled=true adapters.pulsepoint.endpoint=http://localhost:8090/pulsepoint-exchange adapters.pwbid.enabled=true adapters.pwbid.endpoint=http://localhost:8090/pwbid-exchange +adapters.qt.enabled=true +adapters.qt.endpoint=http://localhost:8090/qt-exchange adapters.readpeak.enabled=true adapters.readpeak.endpoint=http://localhost:8090/readpeak-exchange adapters.relevantdigital.enabled=true From 7c61b3964af93b5af85b80ea1c8bbbc1405c16f4 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:50:14 +0200 Subject: [PATCH 007/170] Bugfix: Price Floors Test Fix (#3369) --- .../org/prebid/server/it/PriceFloorsTest.java | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java index d18446e7834..e9ff0df5dd3 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -4,7 +4,6 @@ import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; import org.json.JSONException; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.prebid.server.model.Endpoint; import org.prebid.server.util.IntegrationTestsUtil; @@ -25,7 +24,6 @@ public class PriceFloorsTest extends IntegrationTest { private static final int APP_PORT = 8050; - private static final int WIREMOCK_PORT = 8090; private static final String PRICE_FLOORS = "Price Floors Test"; private static final String FLOORS_FROM_REQUEST = "Floors from request"; @@ -33,14 +31,6 @@ public class PriceFloorsTest extends IntegrationTest { private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); - @BeforeAll - public static void setUpJunk() throws IOException { - WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/periodic-update")) - .willReturn(aResponse().withBody(jsonFrom("storedrequests/test-periodic-refresh.json")))); - WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/currency-rates")) - .willReturn(aResponse().withBody(jsonFrom("currency/latest.json")))); - } - @Test public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IOException, JSONException { // given @@ -61,11 +51,10 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO SPEC); // then - IntegrationTestsUtil.assertJsonEquals( + assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", firstResponse, - singletonList("generic"), - PriceFloorsTest::replaceBidderRelatedStaticInfo); + singletonList("generic")); // given final StubMapping stubMapping = WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) @@ -83,11 +72,10 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO // then assertThat(stubMapping.getNewScenarioState()).isEqualTo(FLOORS_FROM_PROVIDER); - IntegrationTestsUtil.assertJsonEquals( + assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", secondResponse, - singletonList("generic"), - PriceFloorsTest::replaceBidderRelatedStaticInfo); + singletonList("generic")); } @Test @@ -105,14 +93,9 @@ public void openrtb2AuctionShouldSkipPriceFloorsForTheGenericBidderWhenGenericIs SPEC); // then - IntegrationTestsUtil.assertJsonEquals( + assertJsonEquals( "openrtb2/floors/floors-test-auction-response-no-signal.json", firstResponse, - singletonList("generic"), - PriceFloorsTest::replaceBidderRelatedStaticInfo); - } - - private static String replaceBidderRelatedStaticInfo(String json, String bidder) { - return IntegrationTestsUtil.replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT); + singletonList("generic")); } } From 3017465d7b9d3989677a5c16ade72ff200dd98cd Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:51:39 +0200 Subject: [PATCH 008/170] Playdigo: Usersync Support (#3368) --- src/main/resources/bidder-config/playdigo.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/resources/bidder-config/playdigo.yaml b/src/main/resources/bidder-config/playdigo.yaml index 6bc464fea1b..2c8706860f8 100644 --- a/src/main/resources/bidder-config/playdigo.yaml +++ b/src/main/resources/bidder-config/playdigo.yaml @@ -15,3 +15,13 @@ adapters: - native supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: playdigo + redirect: + url: https://cs.playdigo.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + iframe: + url: https://cs.playdigo.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' From af6fcb422f62f41a6f4a2933712b106945122261 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:33:05 +0200 Subject: [PATCH 009/170] Core: Case Insensitive Check for Bidder Controls (#3352) --- .../MultiFormatMediaTypeProcessor.java | 16 ++- .../request/auction/BidderControls.groovy | 3 + .../tests/FilterMultiFormatSpec.groovy | 108 ++++++++++++++++-- .../MultiFormatMediaTypeProcessorTest.java | 34 ++++++ 4 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java index f4e89ea2f76..af0a9e2428c 100644 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java +++ b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.model.BidderError; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -78,10 +80,11 @@ private MediaType preferredMediaType(BidRequest bidRequest, Account account, String originalBidderName, String resolvedBidderName) { + return Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) .map(ExtRequestPrebid::getBiddercontrols) - .map(bidders -> bidders.get(originalBidderName)) + .map(bidders -> getBidder(originalBidderName, bidders)) .map(bidder -> bidder.get(PREF_MTYPE_FIELD)) .filter(JsonNode::isTextual) .map(JsonNode::textValue) @@ -92,6 +95,17 @@ private MediaType preferredMediaType(BidRequest bidRequest, .orElse(null); } + private static JsonNode getBidder(String bidderName, JsonNode biddersNode) { + final Iterator fieldNames = biddersNode.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (StringUtils.equalsIgnoreCase(bidderName, fieldName)) { + return biddersNode.get(fieldName); + } + } + return null; + } + private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { if (!isMultiFormat(imp)) { return imp; diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy index dc01fb12b3a..ee029b56c5b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidderControls.groovy @@ -1,9 +1,12 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class BidderControls { GenericPreferredBidder generic + @JsonProperty("GeNeRiC") + GenericPreferredBidder genericAnyCase } diff --git a/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy index 782b965715d..1d36b8beadc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/FilterMultiFormatSpec.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.Account @@ -11,6 +12,7 @@ import org.prebid.server.functional.model.request.auction.BidderControls import org.prebid.server.functional.model.request.auction.GenericPreferredBidder import org.prebid.server.functional.model.request.auction.Native +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER @@ -52,7 +54,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -62,6 +64,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with one requested preferred media type when default adapters multi format is false in config and preferred media type specified at account level"() { @@ -98,7 +106,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -108,6 +116,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with all requested media type when multi format is true in config and preferred media type specified at request level"() { @@ -119,7 +133,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -129,6 +143,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp.banner assert bidderRequest.imp.audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with all requested media type when multi format is true in config and preferred media type specified at account level"() { @@ -190,7 +210,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -200,6 +220,12 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS should respond with warning and don't make a bidder call when multi format at request and preferred media type specified at account level with non requested media type"() { @@ -241,7 +267,7 @@ class FilterMultiFormatSpec extends BaseSpec { imp[0].banner = null imp[0].audio = Audio.defaultAudio imp[0].nativeObj = Native.defaultNative - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -254,6 +280,12 @@ class FilterMultiFormatSpec extends BaseSpec { assert bidResponse.ext.warnings[GENERIC]?.message == ["Imp ${bidRequest.imp[0].id} does not have a media type after filtering and has been removed from the request for this bidder.", "Bid request contains 0 impressions after filtering."] + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and preferred media type specified at account level"() { @@ -292,7 +324,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = null imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -304,6 +336,12 @@ class FilterMultiFormatSpec extends BaseSpec { and: "Bid response shouldn't contain warning" assert !bidResponse.ext.warnings + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and multi format is false and preferred media type specified at request level with null"() { @@ -315,7 +353,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.getDefaultBanner() imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: NULL)) + ext.prebid.bidderControls = bidderControls } when: "PBS processes auction request" @@ -326,6 +364,12 @@ class FilterMultiFormatSpec extends BaseSpec { and: "Bid response shouldn't contain warning" assert !bidResponse.ext?.warnings + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: NULL)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: NULL)), + ] } def "PBS shouldn't respond with warning and make a bidder call when request doesn't contain multi format and multi format is false and preferred media type specified at account level with null"() { @@ -364,7 +408,7 @@ class FilterMultiFormatSpec extends BaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].banner = Banner.defaultBanner imp[0].audio = Audio.defaultAudio - ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)) + ext.prebid.bidderControls = bidderControls } and: "Account in the DB with preferred media type" @@ -379,5 +423,53 @@ class FilterMultiFormatSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].banner assert !bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] + } + + def "PBS should not preferred media type specified at request level when it's alias bidder"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapter-defaults.ortb.multiformat-supported": "false", + "adapters.generic.ortb.multiformat-supported": "false") + + and: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + banner = Banner.defaultBanner + audio = Audio.defaultAudio + ext.prebid.bidder.tap { + alias = new Generic() + generic = null + } + } + ext.prebid.tap { + it.aliases = [(ALIAS.value): BidderName.GENERIC] + it.bidderControls = bidderControls + } + } + + and: "Account in the DB with preferred media type" + def accountConfig = new AccountAuctionConfig(preferredMediaType: [(BidderName.GENERIC): AUDIO]) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain preferred media type from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.imp[0].banner + assert bidderRequest.imp[0].audio + + where: + bidderControls << [ + new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: BANNER)), + new BidderControls(genericAnyCase: new GenericPreferredBidder(preferredMediaType: BANNER)) + ] } } diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java index 1e89f446502..2e0e0422450 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java @@ -145,6 +145,40 @@ public void processShouldUseRequestLevelPreferredMediaTypeFirst() { assertThat(result.getErrors()).isEmpty(); } + @Test + public void processShouldUseRequestLevelPreferredMediaTypeFirstCaseInsensitive() { + // given + given(bidderAliases.resolveBidder(BIDDER)).willReturn("resolvedBidderName"); + given(bidderCatalog.bidderInfoByName("resolvedBidderName")).willReturn(givenBidderInfo(false)); + + final ObjectNode bidderControls = mapper.createObjectNode(); + bidderControls.putObject(BIDDER.toUpperCase()).put("prefmtype", "video"); + + final BidRequest bidRequest = givenBidRequest( + request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder() + .biddercontrols(bidderControls) + .build())), + givenImp(BANNER, VIDEO, AUDIO, NATIVE)); + + final Account account = givenAccount(Map.of("resolvedBidderName", AUDIO)); + + // when + final MediaTypeProcessingResult result = target.process(bidRequest, BIDDER, bidderAliases, account); + + // then + assertThat(result.isRejected()).isFalse(); + assertThat(result.getBidRequest()) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .containsExactly(givenImp(VIDEO)); + assertThat(result.getBidRequest()) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBiddercontrols) + .isNull(); + assertThat(result.getErrors()).isEmpty(); + } + @Test public void processShouldSkipImpsWithSingleMediaType() { // given From c1bd6fb1d4aafd9f30acf30548d4123f687fa49b Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:24:03 +0200 Subject: [PATCH 010/170] Core: Add More Seat Non Bid Codes (#3240) --- .../ortb2/blocking/core/BidsBlocker.java | 35 ++++ .../Ortb2BlockingRawBidderResponseHook.java | 1 + .../ortb2/blocking/core/BidsBlockerTest.java | 86 +++++++--- .../Ortb2BlockingBidderRequestHookTest.java | 16 +- ...rtb2BlockingRawBidderResponseHookTest.java | 27 ++- .../v1/model/BidderInvocationContextImpl.java | 12 +- .../filter/core/BidResponsesMraidFilter.java | 20 ++- ...diaFilterAllProcessedBidResponsesHook.java | 5 +- .../core/BidResponsesMraidFilterTest.java | 35 +++- ...ilterAllProcessedBidResponsesHookTest.java | 19 +- .../greenbids/model/GreenbidsBid.java | 2 +- .../prebid/server/auction/DsaEnforcer.java | 2 +- .../server/auction/ExchangeService.java | 8 +- .../auction/model/BidRejectionReason.java | 107 ++++++++++-- .../server/bidder/HttpBidderRequester.java | 49 +++++- .../server/bidder/model/BidderError.java | 1 - .../floors/BasicPriceFloorEnforcer.java | 2 +- .../validation/ResponseBidValidator.java | 36 +++- .../config/Ortb2BlockingAttribute.groovy | 13 ++ .../config/Ortb2BlockingAttributes.groovy | 11 ++ .../model/config/Ortb2BlockingConfig.groovy | 11 ++ .../model/config/PbsModulesConfig.groovy | 1 + .../model/request/auction/Imp.groovy | 2 +- .../request/auction/SecurityLevel.groovy | 15 ++ .../auction/BidRejectionReason.groovy | 27 ++- .../functional/tests/BidValidationSpec.groovy | 5 +- .../functional/tests/BidderFormatSpec.groovy | 44 ++--- .../functional/tests/BidderParamsSpec.groovy | 14 +- .../functional/tests/SeatNonBidSpec.groovy | 159 +++++++++++++---- .../ortb2blocking/Ortb2BlockingSpec.groovy | 62 +++++++ .../richmedia/RichMediaFilterSpec.groovy | 54 ++++-- .../pricefloors/PriceFloorsRulesSpec.groovy | 4 +- .../functional/tests/privacy/DsaSpec.groovy | 8 +- .../tests/privacy/GdprAuctionSpec.groovy | 12 +- .../server/auction/DsaEnforcerTest.java | 16 +- .../model/BidRejectionTrackerTest.java | 24 +-- .../bidder/HttpBidderRequesterTest.java | 162 ++++++++++++++++-- .../floors/BasicPriceFloorEnforcerTest.java | 2 +- .../validation/ResponseBidValidatorTest.java | 57 +++++- 39 files changed, 938 insertions(+), 228 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java index 28bdccdd136..6e86d4fce0b 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java @@ -5,6 +5,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.exception.InvalidAccountConfigurationException; @@ -45,6 +47,7 @@ public class BidsBlocker { private final OrtbVersion ortbVersion; private final ObjectNode accountConfig; private final BlockedAttributes blockedAttributes; + private final BidRejectionTracker bidRejectionTracker; private final boolean debugEnabled; private BidsBlocker(List bids, @@ -52,6 +55,7 @@ private BidsBlocker(List bids, OrtbVersion ortbVersion, ObjectNode accountConfig, BlockedAttributes blockedAttributes, + BidRejectionTracker bidRejectionTracker, boolean debugEnabled) { this.bids = bids; @@ -59,6 +63,7 @@ private BidsBlocker(List bids, this.ortbVersion = ortbVersion; this.accountConfig = accountConfig; this.blockedAttributes = blockedAttributes; + this.bidRejectionTracker = bidRejectionTracker; this.debugEnabled = debugEnabled; } @@ -67,6 +72,7 @@ public static BidsBlocker create(List bids, OrtbVersion ortbVersion, ObjectNode accountConfig, BlockedAttributes blockedAttributes, + BidRejectionTracker bidRejectionTracker, boolean debugEnabled) { return new BidsBlocker( @@ -75,6 +81,7 @@ public static BidsBlocker create(List bids, Objects.requireNonNull(ortbVersion), accountConfig, blockedAttributes, + bidRejectionTracker, debugEnabled); } @@ -96,6 +103,10 @@ public ExecutionResult block() { final BlockedBids blockedBids = !blockedBidIndexes.isEmpty() ? BlockedBids.of(blockedBidIndexes) : null; final List warnings = MergeUtils.mergeMessages(blockedBidResults); + if (blockedBids != null) { + rejectBlockedBids(blockedBidResults); + } + return ExecutionResult.builder() .value(blockedBids) .debugMessages(blockedBids != null ? debugMessages(blockedBidIndexes, blockedBidResults) : null) @@ -256,6 +267,30 @@ private String debugEntryFor(int index, BlockingResult blockingResult) { blockingResult.getFailedChecks()); } + private void rejectBlockedBids(List> blockedBidResults) { + blockedBidResults.stream() + .map(Result::getValue) + .filter(BlockingResult::isBlocked) + .forEach(this::rejectBlockedBid); + } + + private void rejectBlockedBid(BlockingResult blockingResult) { + if (blockingResult.getBattrCheckResult().isFailed() + || blockingResult.getBappCheckResult().isFailed() + || blockingResult.getBcatCheckResult().isFailed()) { + + bidRejectionTracker.reject( + blockingResult.getImpId(), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + } + + if (blockingResult.getBadvCheckResult().isFailed()) { + bidRejectionTracker.reject( + blockingResult.getImpId(), + BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + } + } + private List toAnalyticsResults(List> blockedBidResults) { return blockedBidResults.stream() .map(Result::getValue) diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java index 7ba82dd5b09..329e99d3bbc 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java @@ -59,6 +59,7 @@ public Future> call(BidderResponsePayloa ObjectUtils.defaultIfNull(moduleContext.ortbVersionOf(bidder), OrtbVersion.ORTB_2_5), invocationContext.accountConfig(), moduleContext.blockedAttributesFor(bidder), + invocationContext.auctionContext().getBidRejectionTrackers().get(bidder), invocationContext.debugEnabled()) .block(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java index 0e96c78eb3a..a995b43c90a 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java @@ -6,6 +6,11 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.response.Bid; 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.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; @@ -29,7 +34,12 @@ import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +@ExtendWith(MockitoExtension.class) public class BidsBlockerTest { private static final ObjectMapper mapper = new ObjectMapper() @@ -38,14 +48,18 @@ public class BidsBlockerTest { private static final OrtbVersion ORTB_VERSION = OrtbVersion.ORTB_2_5; + @Mock + private BidRejectionTracker bidRejectionTracker; + @Test public void shouldReturnEmptyResultWhenNoBlockingResponseConfig() { // given final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, null, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, null, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -55,12 +69,13 @@ public void shouldReturnEmptyResultWithErrorWhenInvalidAccountConfig() { .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.builder() .errors(singletonList("attributes field in account configuration is not an object")) .build()); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -70,10 +85,11 @@ public void shouldReturnEmptyResultWithoutErrorWhenInvalidAccountConfigAndDebugD .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.empty()); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -88,10 +104,11 @@ public void shouldReturnEmptyResultWhenBidWithoutAdomainAndBlockUnknownFalse() { // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -106,10 +123,11 @@ public void shouldReturnEmptyResultWhenBidWithoutAdomainAndEnforceBlocksFalseAnd // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -124,7 +142,7 @@ public void shouldReturnResultWithBidWhenBidWithoutAdomainAndBlockUnknownTrue() // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); @@ -142,10 +160,11 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndEnforceBlocksFals // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -160,10 +179,11 @@ public void shouldReturnEmptyResultWhenBidWithNotBlockedAdomain() { // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain2.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -178,10 +198,11 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndEnforceBlocksTr // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); } @Test @@ -195,10 +216,11 @@ public void shouldReturnEmptyResultWhenBidWithAdomainAndNoBlockedAttributes() { // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -217,10 +239,11 @@ public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() final BlockedAttributes blockedAttributes = BlockedAttributes.builder() .battr(singletonMap("impId1", asList(1, 2))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -236,10 +259,11 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndInDealsExceptions // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -255,10 +279,11 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndNotInDealsExcep // when final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com")); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); } @Test @@ -273,7 +298,7 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -281,6 +306,7 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { assertThat(result.getDebugMessages()).containsOnly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [bcat]"); }); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); } @Test @@ -295,10 +321,11 @@ public void shouldReturnResultWithBidWithoutDebugMessageWhenBidIsBlockedAndDebug // when final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); } @Test @@ -341,7 +368,7 @@ public void shouldReturnResultWithAnalyticsResults() { .bapp(asList("app1", "app2", "app3")) .battr(singletonMap("impId2", asList(1, 2, 3))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -361,6 +388,11 @@ public void shouldReturnResultWithAnalyticsResults() { AnalyticsResult.of("success-blocked", analyticsResultValues2, "bidder1", "impId2"), AnalyticsResult.of("success-allow", null, "bidder1", "impId1")); }); + + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).reject("impId2", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verifyNoMoreInteractions(bidRejectionTracker); } @Test @@ -411,7 +443,7 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .bapp(asList("app1", "app2")) .battr(singletonMap("impId1", asList(1, 2))) .build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -423,6 +455,10 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { "Bid 5 from bidder bidder1 has been rejected, failed checks: [battr]", "Bid 7 from bidder bidder1 has been rejected, failed checks: [badv, bcat]"); }); + + verify(bidRejectionTracker, times(5)).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTracker, times(2)).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED); + verifyNoMoreInteractions(bidRejectionTracker); } @Test @@ -443,12 +479,14 @@ public void shouldReturnEmptyResultForCattaxIfBidderSupportsLowerThan26() { bid(bid -> bid.cattax(3)), bid()); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, true); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()) .extracting(ExecutionResult::getValue) .isNull(); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -465,12 +503,14 @@ public void shouldPassBidIfCattaxIsNull() { final List bids = singletonList(bid()); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()) .extracting(ExecutionResult::getValue) .isNull(); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -489,7 +529,7 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { bid(bid -> bid.cattax(2))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().cattaxComplement(2).build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -497,6 +537,8 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { assertThat(result.getDebugMessages()).containsExactly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [cattax]"); }); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -515,7 +557,7 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { bid(bid -> bid.cattax(2))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder().build(); final BidsBlocker blocker = BidsBlocker.create( - bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); + bids, "bidder1", OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, bidRejectionTracker, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -523,6 +565,8 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { assertThat(result.getDebugMessages()).containsExactly( "Bid 1 from bidder bidder1 has been rejected, failed checks: [cattax]"); }); + + verifyNoInteractions(bidRejectionTracker); } private static BidderBid bid() { diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index 60dd8ed5867..a66f94ac52c 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; @@ -52,6 +53,9 @@ public class Ortb2BlockingBidderRequestHookTest { @Mock private BidderCatalog bidderCatalog; + @Mock + private BidRejectionTracker bidRejectionTracker; + private Ortb2BlockingBidderRequestHook hook; @BeforeEach @@ -67,7 +71,7 @@ public void shouldReturnResultWithNoActionWhenNoBlockingAttributes() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", null, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, null, true)); // then assertThat(result.succeeded()).isTrue(); @@ -87,7 +91,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -108,7 +112,7 @@ public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndD // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -134,7 +138,7 @@ public void shouldReturnResultWithModuleContextAndPayloadUpdate() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -186,7 +190,7 @@ public void shouldReturnResultWithUpdateActionAndWarning() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -223,7 +227,7 @@ public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java index 1273024247a..0e0a6811835 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java @@ -7,6 +7,11 @@ import com.iab.openrtb.response.Bid; import io.vertx.core.Future; 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.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; @@ -31,6 +36,7 @@ import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.response.BidType; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -39,6 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +@ExtendWith(MockitoExtension.class) public class Ortb2BlockingRawBidderResponseHookTest { private static final ObjectMapper mapper = new ObjectMapper() @@ -48,12 +55,15 @@ public class Ortb2BlockingRawBidderResponseHookTest { private final Ortb2BlockingRawBidderResponseHook hook = new Ortb2BlockingRawBidderResponseHook( ObjectMapperProvider.mapper()); + @Mock + private BidRejectionTracker bidRejectionTracker; + @Test public void shouldReturnResultWithNoActionWhenNoBidsBlocked() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", null, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, null, true)); // then assertThat(result.succeeded()).isTrue(); @@ -83,7 +93,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -104,7 +114,7 @@ public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndD // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -134,6 +144,9 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { BidderInvocationContextImpl.builder() .bidder("bidder1") .accountConfig(accountConfig) + .auctionContext(AuctionContext.builder() + .bidRejectionTrackers(Map.of("bidder1", bidRejectionTracker)) + .build()) .moduleContext(ModuleContext.create().with( "bidder1", BlockedAttributes.builder().badv(singletonList("domain2.com")).build())) .debugEnabled(true) @@ -211,7 +224,7 @@ public void shouldReturnResultWithUpdateActionAndWarning() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -245,7 +258,7 @@ public void shouldReturnResultWithUpdateActionAndNoWarningWhenDebugDisabled() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); @@ -271,7 +284,7 @@ public void shouldReturnResultWithUpdateActionAndDebugMessage() { // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, true)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, true)); // then assertThat(result.succeeded()).isTrue(); @@ -297,7 +310,7 @@ public void shouldReturnResultWithUpdateActionAndNoDebugMessageWhenDebugDisabled // when final Future> result = hook.call( BidderResponsePayloadImpl.of(singletonList(bid())), - BidderInvocationContextImpl.of("bidder1", accountConfig, false)); + BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, accountConfig, false)); // then assertThat(result.succeeded()).isTrue(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java index 99d19db08da..d11d1912598 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java @@ -5,10 +5,13 @@ import lombok.Value; import lombok.experimental.Accessors; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.execution.Timeout; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.model.Endpoint; +import java.util.Map; + @Accessors(fluent = true) @Builder @Value @@ -28,9 +31,16 @@ public class BidderInvocationContextImpl implements BidderInvocationContext { String bidder; - public static BidderInvocationContext of(String bidder, ObjectNode accountConfig, boolean debugEnabled) { + public static BidderInvocationContext of(String bidder, + BidRejectionTracker bidRejectionTracker, + ObjectNode accountConfig, + boolean debugEnabled) { + return BidderInvocationContextImpl.builder() .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) + .build()) .accountConfig(accountConfig) .debugEnabled(debugEnabled) .build(); diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java index 208625e715b..e528ce69c4e 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java @@ -2,6 +2,8 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -21,7 +23,10 @@ public class BidResponsesMraidFilter { private static final String TAG_STATUS = "success-block"; private static final Map TAG_VALUES = Map.of("richmedia-format", "mraid"); - public MraidFilterResult filterByPattern(String mraidScriptPattern, List responses) { + public MraidFilterResult filterByPattern(String mraidScriptPattern, + List responses, + Map bidRejectionTrackers) { + List filteredResponses = new ArrayList<>(); List analyticsResults = new ArrayList<>(); @@ -41,18 +46,21 @@ public MraidFilterResult filterByPattern(String mraidScriptPattern, List errors = new ArrayList<>(seatBid.getErrors()); - errors.add(BidderError.of( - "Invalid creatives", - BidderError.Type.invalid_creative, - new HashSet<>(rejectedImps))); + errors.add(BidderError.of("Invalid bid", BidderError.Type.invalid_bid, new HashSet<>(rejectedImps))); filteredResponses.add(bidderResponse.with(seatBid.with(validBids, errors))); } } diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java index 3465eb08fc4..ee92a5e2064 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java @@ -58,7 +58,10 @@ public Future> call( final List responses = allProcessedBidResponsesPayload.bidResponses(); if (BooleanUtils.isTrue(properties.getFilterMraid())) { - final MraidFilterResult filterResult = mraidFilter.filterByPattern(properties.getMraidScriptPattern(), responses); + final MraidFilterResult filterResult = mraidFilter.filterByPattern( + properties.getMraidScriptPattern(), + responses, + auctionInvocationContext.auctionContext().getBidRejectionTrackers()); final InvocationAction action = filterResult.hasRejectedBids() ? InvocationAction.update : InvocationAction.no_action; diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java index 1997372757d..f2066474df9 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java @@ -2,6 +2,8 @@ import com.iab.openrtb.response.Bid; import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -14,6 +16,10 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class BidResponsesMraidFilterTest { @@ -24,14 +30,21 @@ public void filterShouldReturnOriginalBidsWhenNoBidsHaveMraidScriptInAdm() { // given final BidderResponse responseA = givenBidderResponse("bidderA", List.of(givenBid("imp_id", "adm1"))); final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid("imp_id", "adm2"))); + final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class); + final Map givenTrackers = Map.of( + "bidderA", bidRejectionTrackerA, + "bidderB", bidRejectionTrackerB); // when - final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB)); + final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB), givenTrackers); // then assertThat(filterResult.getFilterResult()).containsExactly(responseA, responseB); assertThat(filterResult.getAnalyticsResult()).isEmpty(); assertThat(filterResult.hasRejectedBids()).isFalse(); + + verifyNoInteractions(bidRejectionTrackerA, bidRejectionTrackerB); } @Test @@ -47,10 +60,19 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() givenBid("imp_id1", "adm1_mraid.js"), givenBid("imp_id2", "adm2_mraid.js"))); + final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class); + final BidRejectionTracker bidRejectionTrackerC = mock(BidRejectionTracker.class); + final Map givenTrackers = Map.of( + "bidderA", bidRejectionTrackerA, + "bidderB", bidRejectionTrackerB, + "bidderC", bidRejectionTrackerC); + // when final MraidFilterResult filterResult = target.filterByPattern( "mraid.js", - List.of(responseA, responseB, responseC)); + List.of(responseA, responseB, responseC), + givenTrackers); // then final BidderResponse expectedResponseA = givenBidderResponse( @@ -81,6 +103,13 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() assertThat(filterResult.getAnalyticsResult()) .containsExactlyInAnyOrder(expectedAnalyticsResultB, expectedAnalyticsResultC); assertThat(filterResult.hasRejectedBids()).isTrue(); + + verifyNoInteractions(bidRejectionTrackerA); + verify(bidRejectionTrackerB) + .reject(List.of("imp_id2"), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verify(bidRejectionTrackerC) + .reject(List.of("imp_id1", "imp_id2"), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); + verifyNoMoreInteractions(bidRejectionTrackerB, bidRejectionTrackerC); } private static BidderResponse givenBidderResponse(String bidder, List bids) { @@ -96,7 +125,7 @@ private static BidderBid givenBid(String impId, String adm) { } private static BidderError givenError(String... rejectedImps) { - return BidderError.of("Invalid creatives", BidderError.Type.invalid_creative, Set.of(rejectedImps)); + return BidderError.of("Invalid bid", BidderError.Type.invalid_bid, Set.of(rejectedImps)); } } diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java index 8e8b67ea289..47d5ab27253 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; @@ -48,7 +50,7 @@ public class PbRichmediaFilterAllProcessedBidResponsesHookTest { @Mock private AllProcessedBidResponsesPayload allProcessedBidResponsesPayload; - @Mock + @Mock(strictness = LENIENT) private AuctionInvocationContext auctionInvocationContext; @Mock @@ -59,10 +61,15 @@ public class PbRichmediaFilterAllProcessedBidResponsesHookTest { private PbRichmediaFilterAllProcessedBidResponsesHook target; + private static final Map BID_REJECTION_TRACKERS = Map.of( + "bidder", new BidRejectionTracker("bidder", Collections.emptySet(), 0.1)); + @BeforeEach public void setUp() { target = new PbRichmediaFilterAllProcessedBidResponsesHook(ObjectMapperProvider.mapper(), mraidFilter, configResolver); when(configResolver.resolve(any())).thenReturn(PbRichMediaFilterProperties.of(true, "pattern")); + when(auctionInvocationContext.auctionContext()) + .thenReturn(AuctionContext.builder().bidRejectionTrackers(BID_REJECTION_TRACKERS).build()); } @Test @@ -103,7 +110,7 @@ public void callShouldReturnResultWithUpdateActionWhenSomeResponsesWereFilteredO // given final List givenResponses = givenBidderResponses(2); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, List.of(givenAnalyticsResult("bidder", "imp_id")))); // when @@ -126,7 +133,7 @@ public void callShouldReturnResultWithNoActionWhenNothingWereFilteredOut() { // given final List givenResponses = givenBidderResponses(2); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, Collections.emptyList())); // when @@ -150,7 +157,7 @@ public void callShouldReturnResultOfFilteredResponses() { final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); final List expectedResponses = givenBidderResponses(2); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(expectedResponses, Collections.emptyList())); // when @@ -174,7 +181,7 @@ public void callShouldReturnAnalyticsResultsOfRejectedBids() { // given final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of( givenResponses, List.of( @@ -219,7 +226,7 @@ public void callShouldReturnEmptyAnalyticsResultsWhenThereAreNoRejectedBids() { // given final List givenResponses = givenBidderResponses(3); doReturn(givenResponses).when(allProcessedBidResponsesPayload).bidResponses(); - given(mraidFilter.filterByPattern("pattern", givenResponses)) + given(mraidFilter.filterByPattern("pattern", givenResponses, BID_REJECTION_TRACKERS)) .willReturn(MraidFilterResult.of(givenResponses, Collections.emptyList())); // when diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java index 2e47e071bec..f65e2f1bf80 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsBid.java @@ -42,7 +42,7 @@ public static GreenbidsBid ofBid(String seat, Bid bid, JsonNode params, String c public static GreenbidsBid ofNonBid(String seat, NonBid nonBid, JsonNode params, String currency) { return GreenbidsBid.builder() .bidder(seat) - .isTimeout(nonBid.getStatusCode() == BidRejectionReason.TIMED_OUT) + .isTimeout(nonBid.getStatusCode() == BidRejectionReason.ERROR_TIMED_OUT) .hasBid(false) .params(params) .currency(currency) diff --git a/src/main/java/org/prebid/server/auction/DsaEnforcer.java b/src/main/java/org/prebid/server/auction/DsaEnforcer.java index b9b87abd37c..6719829cc56 100644 --- a/src/main/java/org/prebid/server/auction/DsaEnforcer.java +++ b/src/main/java/org/prebid/server/auction/DsaEnforcer.java @@ -72,7 +72,7 @@ public AuctionParticipation enforce(BidRequest bidRequest, } } catch (PreBidException e) { warnings.add(BidderError.invalidBid("Bid \"%s\": %s".formatted(bid.getId(), e.getMessage()))); - rejectionTracker.reject(bid.getImpid(), BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + rejectionTracker.reject(bid.getImpid(), BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); updatedBidderBids.remove(bidderBid); } } diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index ed537c6746c..bdade14ab8a 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -832,7 +832,7 @@ private AuctionParticipation createAuctionParticipation( if (blockedRequestByTcf) { context.getBidRejectionTrackers() .get(bidder) - .rejectAll(BidRejectionReason.REJECTED_BY_PRIVACY); + .rejectAll(BidRejectionReason.REQUEST_BLOCKED_PRIVACY); return AuctionParticipation.builder() .bidder(bidder) @@ -1240,7 +1240,7 @@ private Future processAndRequestBids(AuctionContext auctionConte if (mediaTypeProcessingResult.isRejected()) { auctionContext.getBidRejectionTrackers() .get(bidderName) - .rejectAll(BidRejectionReason.REJECTED_BY_MEDIA_TYPE); + .rejectAll(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE); final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() .warnings(mediaTypeProcessingErrors) .build(); @@ -1287,7 +1287,7 @@ private Future requestBidsOrRejectBidder( if (hookStageResult.isShouldReject()) { auctionContext.getBidRejectionTrackers() .get(bidderRequest.getBidder()) - .rejectAll(BidRejectionReason.REJECTED_BY_HOOK); + .rejectAll(BidRejectionReason.REQUEST_BLOCKED_GENERAL); return Future.succeededFuture(BidderResponse.of(bidderRequest.getBidder(), BidderSeatBid.empty(), 0)); } @@ -1657,7 +1657,7 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { case failed_to_request_bids -> MetricName.failedtorequestbids; case timeout -> MetricName.timeout; case invalid_bid -> MetricName.bid_validation; - case rejected_ipf, generic, invalid_creative -> MetricName.unknown_error; + case rejected_ipf, generic -> MetricName.unknown_error; }; } diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index de2e89efc31..70fd0244bc5 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -1,20 +1,100 @@ package org.prebid.server.auction.model; import com.fasterxml.jackson.annotation.JsonValue; -import org.prebid.server.bidder.model.BidderError; +/** + * The list of the Seat Non Bid codes: + * 0 - the bidder is called but declines to bid and doesn't provide a code (for the impression) + * 100-199 - the bidder is called but returned with an unspecified error (for the impression) + * 200-299 - the bidder is not called at all + * 300-399 - the bidder is called, but its response is rejected + */ public enum BidRejectionReason { + /** + * If the bidder returns in time but declines to bid and doesn’t provide an “NBR” code. + */ NO_BID(0), - TIMED_OUT(101), - REJECTED_BY_HOOK(200), - REJECTED_BY_PRIVACY(202), - REJECTED_BY_MEDIA_TYPE(204), - GENERAL(300), - REJECTED_DUE_TO_PRICE_FLOOR(301), - REJECTED_BY_DSA_PRIVACY(305), - FAILED_TO_REQUEST_BIDS(100), - OTHER_ERROR(100); + + /** + * The bidder returned with an unspecified error for this impression. + * Applied if any other ERROR is not recognized. + */ + ERROR_GENERAL(100), + + /** + * The bidder failed because of timeout + */ + ERROR_TIMED_OUT(101), + + /** + * The bidder returned status code less than 200 OR greater than or equal to 400 + */ + ERROR_INVALID_BID_RESPONSE(102), + + /** + * The bidder returned HTTP 503 + */ + ERROR_BIDDER_UNREACHABLE(103), + + /** + * The bidder is not called at all. + * Applied if any other REQUEST_BLOCKED reason is not recognized. + */ + REQUEST_BLOCKED_GENERAL(200), + + /** + * If the request was not sent to the bidder because they don’t support dooh or app + */ + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + + /** + * This impression not sent to the bid adapter because it doesn’t support the requested mediatype. + */ + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + + /** + * If the bidder was not called due to GDPR purpose 2 + */ + REQUEST_BLOCKED_PRIVACY(204), + + /** + * The bidder is called, but its response is rejected. + * Applied if any other RESPONSE_REJECTED reason is not recognized. + */ + RESPONSE_REJECTED_GENERAL(300), + + /** + * The bidder returns a bid that doesn't meet the price floor. + */ + RESPONSE_REJECTED_BELOW_FLOOR(301), + + /** + * Rejected by the DSA validations + */ + RESPONSE_REJECTED_DSA_PRIVACY(305), + + /** + * If the ortbblocking module enforced a bid response for battr, bcat, bapp, btype. + * If the richmedia module filtered out a bid response. + */ + RESPONSE_REJECTED_INVALID_CREATIVE(350), + + /** + * If a bid response was rejected due to size. + * When the auction.bid-validations.banner-creative-max-size is in enforce mode and rejects a bid. + */ + RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED(351), + + /** + * If a bid response was rejected due to auction.validations.secure-markup + */ + RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE(352), + + /** + * If the ortbblocking module enforced a bid response due to badv + */ + RESPONSE_REJECTED_ADVERTISER_BLOCKED(356); public final int code; @@ -27,11 +107,4 @@ private int getValue() { return code; } - public static BidRejectionReason fromBidderError(BidderError error) { - return switch (error.getType()) { - case timeout -> BidRejectionReason.TIMED_OUT; - case rejected_ipf -> BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR; - default -> BidRejectionReason.OTHER_ERROR; - }; - } } diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 02d2632d6d6..2a2fde46430 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -100,7 +100,8 @@ public Future requestBids(Bidder bidder, final List errors = httpRequestsWithErrors.getErrors(); final List> httpRequests = enrichRequests( bidderName, httpRequestsWithErrors.getValue(), requestHeaders, aliases, bidRequest); - recordBidderProvidedErrors(bidRejectionTracker, errors); + + rejectErrors(bidRejectionTracker, errors, BidRejectionReason.REQUEST_BLOCKED_GENERAL); if (CollectionUtils.isEmpty(httpRequests)) { return emptyBidderSeatBidWithErrors(errors); @@ -144,11 +145,13 @@ private List> enrichRequests(String bidderName, .toList(); } - private static void recordBidderProvidedErrors(BidRejectionTracker rejectionTracker, List errors) { - errors.stream() + private static void rejectErrors(BidRejectionTracker bidRejectionTracker, + List bidderErrors, + BidRejectionReason reason) { + + bidderErrors.stream() .filter(error -> CollectionUtils.isNotEmpty(error.getImpIds())) - .forEach(error -> rejectionTracker.reject( - error.getImpIds(), BidRejectionReason.fromBidderError(error))); + .forEach(error -> bidRejectionTracker.reject(error.getImpIds(), reason)); } private boolean isStoredResponse(List> httpRequests, String storedResponse, String bidder) { @@ -374,16 +377,44 @@ private void handleBidderErrors(CompositeBidderResponse bidderResponse) { final List bidderErrors = bidderResponse != null ? bidderResponse.getErrors() : null; if (bidderErrors != null) { errorsRecorded.addAll(bidderErrors); - recordBidderProvidedErrors(bidRejectionTracker, bidderErrors); + rejectErrors(bidRejectionTracker, bidderErrors, BidRejectionReason.ERROR_GENERAL); } } private void handleBidderCallError(BidderCall bidderCall) { + final Set requestedImpIds = bidderCall.getRequest().getImpIds(); + if (CollectionUtils.isEmpty(requestedImpIds)) { + return; + } + + final Integer statusCode = Optional.ofNullable(bidderCall.getResponse()) + .map(HttpResponse::getStatusCode) + .orElse(null); + + if (statusCode != null && statusCode == HttpResponseStatus.SERVICE_UNAVAILABLE.code()) { + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + return; + } + + if (statusCode != null + && (statusCode < HttpResponseStatus.OK.code() + || statusCode >= HttpResponseStatus.BAD_REQUEST.code())) { + + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + return; + } + final BidderError callError = bidderCall.getError(); final BidderError.Type callErrorType = callError != null ? callError.getType() : null; - final Set requestedImpIds = bidderCall.getRequest().getImpIds(); - if (callErrorType != null && CollectionUtils.isNotEmpty(requestedImpIds)) { - bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.fromBidderError(callError)); + + if (callErrorType == null) { + return; + } + + if (callErrorType == BidderError.Type.timeout) { + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_TIMED_OUT); + } else { + bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_GENERAL); } } diff --git a/src/main/java/org/prebid/server/bidder/model/BidderError.java b/src/main/java/org/prebid/server/bidder/model/BidderError.java index bf6fa169f7e..0873570a6d3 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderError.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderError.java @@ -94,7 +94,6 @@ public enum Type { * Covers the case where a bid was rejected by price-floors feature functionality */ rejected_ipf(6), - invalid_creative(350), timeout(1), generic(999); diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 04d0188400d..ee77a6bbcb5 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -179,7 +179,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, "Bid with id '%s' was rejected by floor enforcement: price %s is below the floor %s" .formatted(bid.getId(), price, floor), impId)); - rejectionTracker.reject(impId, BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); + rejectionTracker.reject(impId, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); updatedBidderBids.remove(bidderBid); } } diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java index 17132e1bc35..10b939ae5dd 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -11,6 +11,8 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; @@ -72,6 +74,7 @@ public ValidationResult validate(BidderBid bidderBid, final Bid bid = bidderBid.getBid(); final BidRequest bidRequest = auctionContext.getBidRequest(); final Account account = auctionContext.getAccount(); + final BidRejectionTracker bidRejectionTracker = auctionContext.getBidRejectionTrackers().get(bidder); final List warnings = new ArrayList<>(); try { @@ -81,10 +84,25 @@ public ValidationResult validate(BidderBid bidderBid, final Imp correspondingImp = findCorrespondingImp(bid, bidRequest); if (bidderBid.getType() == BidType.banner) { - warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases)); + warnings.addAll(validateBannerFields( + bid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); } - warnings.addAll(validateSecureMarkup(bid, bidder, bidRequest, account, correspondingImp, aliases)); + warnings.addAll(validateSecureMarkup( + bid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); + } catch (ValidationException e) { return ValidationResult.error(warnings, e.getMessage()); } @@ -148,7 +166,8 @@ private List validateBannerFields(Bid bid, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { final BidValidationEnforcement bannerMaxSizeEnforcement = effectiveBannerMaxSizeEnforcement(account); if (bannerMaxSizeEnforcement != BidValidationEnforcement.skip) { @@ -170,6 +189,10 @@ private List validateBannerFields(Bid bid, bid.getW(), bid.getH()); + bidRejectionTracker.reject( + correspondingImp.getId(), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); + return singleWarningOrValidationException( bannerMaxSizeEnforcement, metricName -> metrics.updateSizeValidationMetrics( @@ -218,7 +241,8 @@ private List validateSecureMarkup(Bid bid, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { if (secureMarkupEnforcement == BidValidationEnforcement.skip) { return Collections.emptyList(); @@ -234,6 +258,10 @@ private List validateSecureMarkup(Bid bid, creative validation for bid %s, account=%s, referrer=%s, adm=%s""" .formatted(secureMarkupEnforcement, bidder, bid.getId(), accountId, referer, adm); + bidRejectionTracker.reject( + correspondingImp.getId(), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); + return singleWarningOrValidationException( secureMarkupEnforcement, metricName -> metrics.updateSecureValidationMetrics( diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy new file mode 100644 index 00000000000..827559c17cd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingAttribute { + + Boolean enforceBlocks + List blockedAdomain +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy new file mode 100644 index 00000000000..e5a3c13f5d9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Ortb2BlockingAttributes { + + Ortb2BlockingAttribute badv +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy new file mode 100644 index 00000000000..fbbe08089fc --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Ortb2BlockingConfig { + + Ortb2BlockingAttributes attributes +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 33b2e1478ad..74a6ddab94d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -10,4 +10,5 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter class PbsModulesConfig { RichmediaFilter pbRichmediaFilter + Ortb2BlockingConfig ortb2Blocking } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index 0e6195c977b..8ea26570de9 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -35,7 +35,7 @@ class Imp { BigDecimal bidFloor Currency bidFloorCur Integer clickBrowser - Integer secure + SecurityLevel secure List iframeBuster Integer rwdd Integer ssai diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy new file mode 100644 index 00000000000..8ca88307215 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SecurityLevel.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum SecurityLevel { + + NON_SECURE(0), SECURE(1) + + @JsonValue + private final Integer level + + SecurityLevel(int level) { + this.level = level + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index 6cc4fc5605a..3f14bac3db1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy @@ -4,15 +4,24 @@ import com.fasterxml.jackson.annotation.JsonValue enum BidRejectionReason { - NO_BID(0), - TIMED_OUT(101), - REJECTED_BY_HOOK(200), - REJECTED_BY_PRIVACY(202), - REJECTED_BY_MEDIA_TYPE(204), - GENERAL(300), - REJECTED_DUE_TO_PRICE_FLOOR(301), - REJECTED_DUE_TO_DSA(305), - OTHER_ERROR(100) + ERROR_NO_BID(0), + ERROR_GENERAL(100), + ERROR_TIMED_OUT(101), + ERROR_INVALID_BID_RESPONSE(102), + ERROR_BIDDER_UNREACHABLE(103), + + REQUEST_BLOCKED_GENERAL(200), + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + REQUEST_BLOCKED_PRIVACY(204), + + RESPONSE_REJECTED_GENERAL(300), + RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR(301), + RESPONSE_REJECTED_DUE_TO_DSA(305), + RESPONSE_REJECTED_INVALID_CREATIVE(350), + RESPONSE_REJECTED_INVALID_CREATIVE_SIZE(351), + RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE(352), + RESPONSE_REJECTED_ADVERTISER_BLOCKED(356) @JsonValue final Integer code diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy index fb3e2a3b0aa..b2cda35dde1 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy @@ -269,12 +269,15 @@ class BidValidationSpec extends BaseSpec { } bidder.setResponse(bidRequest.id, bidResponse) + and: "Flush metric" + flushMetrics(defaultPbsService) + when: "Sending auction request to PBS" defaultPbsService.sendAuctionRequest(bidRequest) then: "Bid validation metric value is incremented" def metrics = defaultPbsService.sendCollectedMetricsRequest() - assert metrics["adapter.generic.requests.bid_validation"] == initialMetricValue + 1 + assert metrics["adapter.generic.requests.bid_validation"] == 1 } def "PBS shouldn't throw error when two separate eids with same eids.source"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy index deec160409d..b6cfe6d0313 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy @@ -26,6 +26,8 @@ import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE import static org.prebid.server.functional.model.config.BidValidationEnforcement.SKIP import static org.prebid.server.functional.model.config.BidValidationEnforcement.WARN +import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC class BidderFormatSpec extends BaseSpec { @@ -580,13 +582,13 @@ class BidderFormatSpec extends BaseSpec { assert !bidder.getBidderRequests(bidRequest.id) where: - secure | secureMarkup - 1 | SKIP.value - 1 | ENFORCE.value - 1 | WARN.value - 0 | SKIP.value - 0 | ENFORCE.value - 0 | WARN.value + secure | secureMarkup + SECURE | SKIP.value + SECURE | ENFORCE.value + SECURE | WARN.value + NON_SECURE | SKIP.value + NON_SECURE | ENFORCE.value + NON_SECURE | WARN.value } def "PBS should emit metrics and error when imp[0].secure = 1 and config WARN and bid response adm contain #url"() { @@ -596,7 +598,7 @@ class BidderFormatSpec extends BaseSpec { and: "Default bid request with secure and banner or video or nativeObj" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].secure = 1 + imp[0].secure = SECURE imp[0].banner = banner imp[0].video = video imp[0].nativeObj = nativeObj @@ -654,7 +656,7 @@ class BidderFormatSpec extends BaseSpec { and: "Default bid request with secure and banner or video or nativeObj" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].secure = 1 + imp[0].secure = SECURE imp[0].banner = banner imp[0].video = video imp[0].nativeObj = nativeObj @@ -704,7 +706,7 @@ class BidderFormatSpec extends BaseSpec { and: "Default bid request with secure and banner or video or nativeObj" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].secure = 1 + imp[0].secure = SECURE imp[0].banner = banner imp[0].video = video imp[0].nativeObj = nativeObj @@ -796,16 +798,16 @@ class BidderFormatSpec extends BaseSpec { assert !bidder.getBidderRequests(bidRequest.id) where: - url | secure | secureMarkup - "http%3A" | 0 | SKIP.value - "http" | 0 | SKIP.value - "https" | 1 | SKIP.value - "http%3A" | 0 | WARN.value - "http" | 0 | WARN.value - "https" | 1 | WARN.value - "http%3A" | 0 | ENFORCE.value - "http" | 0 | ENFORCE.value - "https" | 1 | ENFORCE.value + url | secure | secureMarkup + "http%3A" | NON_SECURE | SKIP.value + "http" | NON_SECURE | SKIP.value + "https" | SECURE | SKIP.value + "http%3A" | NON_SECURE | WARN.value + "http" | NON_SECURE | WARN.value + "https" | SECURE | WARN.value + "http%3A" | NON_SECURE | ENFORCE.value + "http" | NON_SECURE | ENFORCE.value + "https" | SECURE | ENFORCE.value } def "PBS should ignore specified secureMarkup #secureMarkup validation when secure is 0"() { @@ -816,7 +818,7 @@ class BidderFormatSpec extends BaseSpec { def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].tap { - secure = 0 + secure = NON_SECURE ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: BidderName.GENERIC)] } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index c31570e26ed..ff1ce756ac8 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -35,6 +35,8 @@ import static org.prebid.server.functional.model.request.auction.Asset.titleAsse import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER @@ -701,9 +703,9 @@ class BidderParamsSpec extends BaseSpec { where: secureStoredRequest | secureBidderRequest - null | 1 - 1 | 1 - 0 | 0 + null | SECURE + SECURE | SECURE + NON_SECURE | NON_SECURE } def "PBS auction should populate imp[0].secure depend which value in imp request"() { @@ -721,9 +723,9 @@ class BidderParamsSpec extends BaseSpec { where: secureRequest | secureBidderRequest - null | 1 - 1 | 1 - 0 | 0 + null | SECURE + SECURE | SECURE + NON_SECURE | NON_SECURE } def "PBS shouldn't emit warning and proceed auction when imp.ext.anyUnsupportedBidder and imp.ext.prebid.bidder.generic in the request"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy index 9e942f7c3c7..7acfb56d2ca 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy @@ -1,28 +1,44 @@ package org.prebid.server.functional.tests import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountBidValidationConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.util.PBSUtils +import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 import static org.mockserver.model.HttpStatusCode.OK_200 -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.NO_BID -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.OTHER_ERROR -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_MEDIA_TYPE -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.TIMED_OUT +import static org.mockserver.model.HttpStatusCode.PROCESSING_102 +import static org.mockserver.model.HttpStatusCode.SERVICE_UNAVAILABLE_503 +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_BIDDER_UNREACHABLE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_INVALID_BID_RESPONSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_NO_BID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_TIMED_OUT +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC class SeatNonBidSpec extends BaseSpec { def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder didn't bid"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true - } + def bidRequest = requestWithAllBidStatus and: "Default bidder response without bid" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -41,24 +57,21 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == NO_BID + assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID where: responseStatusCode << [OK_200, NO_CONTENT_204] } - def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with non-SUCCESS status code"() { + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with invalid bid response status code"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true - } + def bidRequest = requestWithAllBidStatus - and: "Default bidder response without bid" + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) and: "Set bidder response" - def successStatuses = [OK_200, NO_CONTENT_204] - def statusCode = PBSUtils.getRandomElement(HttpStatusCode.values() - successStatuses as List) + def statusCode = PBSUtils.getRandomElement([PROCESSING_102, BAD_REQUEST_400, INTERNAL_SERVER_ERROR_500]) bidder.setResponse(bidRequest.id, bidResponse, statusCode) when: "PBS processes auction request" @@ -70,16 +83,100 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == OTHER_ERROR + assert seatNonBid.nonBid[0].statusCode == ERROR_INVALID_BID_RESPONSE } - def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() { + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with bidder unreachable status code"() { given: "Default bid request with returnAllBidStatus" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse, SERVICE_UNAVAILABLE_503) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == ERROR_BIDDER_UNREACHABLE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with invalid creative size status code"() { + given: "Default bid request with returnAllBidStatus" + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response with creative size adjustment" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.tap { + bid.first.height = bidRequest.imp.first.banner.format.first.height + 1 + bid.first.weight = bidRequest.imp.first.banner.format.first.weight + 1 + } } - and: "Default bidder response without bid" + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(bidValidations: + new AccountBidValidationConfig(bannerMaxSizeEnforcement: ENFORCE))) + def account = new Account(status: ACTIVE, uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_SIZE + } + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with not secure status code"() { + given: "PBS with secure-markup enforcement" + def pbsService = pbsServiceFactory.getService(["auction.validations.secure-markup": ENFORCE.value]) + + and: "A bid request with secure and returnAllBidStatus flags set" + def bidRequest = requestWithAllBidStatus.tap { + imp[0].secure = SECURE + } + + and: "A default bidder response without a valid bid" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.bid.first.tap { + it.adm = new Adm(assets: [Asset.getImgAsset("http://secure-assets.${PBSUtils.randomString}.com")]) + } + } + + and: "Setting the bidder response" + bidder.setResponse(bidRequest.id, storedBidResponse) + + when: "PBS processes the auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "The PBS response should contain seatNonBid for the called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE + } + + def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() { + given: "Default bid request with returnAllBidStatus" + def bidRequest = requestWithAllBidStatus + + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) and: "Set bidder response" @@ -95,12 +192,11 @@ class SeatNonBidSpec extends BaseSpec { def "PBS should populate seatNonBid when returnAllBidStatus=true and debug=#debug and requested bidder didn't bid for any reason"() { given: "Default bid request with returnAllBidStatus and debug = #debug" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus.tap { ext.prebid.debug = debug } - and: "Default bidder response without bid" + and: "Default bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { seatbid = [] } @@ -117,7 +213,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == NO_BID + assert seatNonBid.nonBid[0].statusCode == ERROR_NO_BID and: "PBS response shouldn't contain seatBid" assert !response.seatbid @@ -158,8 +254,7 @@ class SeatNonBidSpec extends BaseSpec { def pbsService = pbsServiceFactory.getService(["auction.biddertmax.min": timeout as String]) and: "Default bid request with max timeout" - def bidRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.returnAllBidStatus = true + def bidRequest = requestWithAllBidStatus.tap { tmax = timeout } @@ -179,7 +274,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == TIMED_OUT + assert seatNonBid.nonBid[0].statusCode == ERROR_TIMED_OUT } def "PBS should populate seatNonBid when filter-imp-media-type=true and imp doesn't contain supported media type"() { @@ -203,7 +298,7 @@ class SeatNonBidSpec extends BaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_BY_MEDIA_TYPE + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE and: "seatbid should be empty" assert response.seatbid.isEmpty() @@ -230,4 +325,10 @@ class SeatNonBidSpec extends BaseSpec { assert !response.ext.seatnonbid assert response.seatbid } + + private static BidRequest getRequestWithAllBidStatus(DistributionChannel channel = SITE) { + BidRequest.getDefaultBidRequest(channel).tap { + ext.prebid.returnAllBidStatus = true + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy new file mode 100644 index 00000000000..d5eec884e9f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -0,0 +1,62 @@ +package org.prebid.server.functional.tests.module.ortb2blocking + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingAttribute +import org.prebid.server.functional.model.config.Ortb2BlockingAttributes +import org.prebid.server.functional.model.config.Ortb2BlockingConfig +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC + +class Ortb2BlockingSpec extends ModuleBaseSpec { + + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings) + + def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with rejected advertiser blocked status code"() { + given: "Default account with return bid status" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.returnAllBidStatus = true + } + + and: "Default bidder response with aDomain" + def aDomain = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.adomain = [aDomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with blocking configuration" + def blockingAttributes = new Ortb2BlockingAttributes(badv: new Ortb2BlockingAttribute(enforceBlocks: true, blockedAdomain: [aDomain])) + def blockingConfig = new Ortb2BlockingConfig(attributes: blockingAttributes) + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [BIDDER_REQUEST, RAW_BIDDER_RESPONSE]) + def richMediaFilterConfig = new PbsModulesConfig(ortb2Blocking: blockingConfig) + def accountHooksConfig = new AccountHooksConfiguration(executionPlan: executionPlan, modules: richMediaFilterConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy index c14acd7d95d..b12ff9644b4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy @@ -12,11 +12,11 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.AnalyticResult import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION @@ -30,7 +30,8 @@ class RichMediaFilterSpec extends ModuleBaseSpec { private final PrebidServerService pbsServiceWithEnabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME)) private final PrebidServerService pbsServiceWithEnabledMediaFilterAndDifferentCaseStrategy = pbsServiceFactory.getService( (getRichMediaFilterSettings(PATTERN_NAME) + ["hooks.host-execution-plan": encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]).tap { - endpoints.values().first().stages.values().first().groups.first.hookSequenceSnakeCase = [new HookId(moduleCodeSnakeCase: PB_RICHMEDIA_FILTER.code, hookImplCodeSnakeCase: "${PB_RICHMEDIA_FILTER.code}-${ALL_PROCESSED_BID_RESPONSES.value}-hook")]})]) + endpoints.values().first().stages.values().first().groups.first.hookSequenceSnakeCase = [new HookId(moduleCodeSnakeCase: PB_RICHMEDIA_FILTER.code, hookImplCodeSnakeCase: "${PB_RICHMEDIA_FILTER.code}-${ALL_PROCESSED_BID_RESPONSES.value}-hook")] + })]) .collectEntries { key, value -> [(key.toString()): value.toString()] }) private final PrebidServerService pbsServiceWithDisabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME, false)) @@ -38,6 +39,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -73,6 +75,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -95,10 +98,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -114,6 +119,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -151,6 +157,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -175,10 +182,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -194,6 +203,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -231,6 +241,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -255,10 +266,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) @@ -271,6 +284,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -308,6 +322,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { and: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -345,6 +360,7 @@ class RichMediaFilterSpec extends ModuleBaseSpec { given: "BidRequest with stored response" def storedResponseId = PBSUtils.randomNumber def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.returnAllBidStatus = true it.ext.prebid.trace = VERBOSE it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] } @@ -367,10 +383,12 @@ class RichMediaFilterSpec extends ModuleBaseSpec { assert !response.seatbid and: "Response should contain error of invalid creation for imp with code 350" - def responseErrors = response.ext.errors - assert responseErrors[ErrorType.GENERIC]*.message == ['Invalid creatives'] - assert responseErrors[ErrorType.GENERIC]*.code == [350] - assert responseErrors[ErrorType.GENERIC].collectMany { it.impIds } == bidRequest.imp.id + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE and: "Add an entry to the analytics tag for this rejected bid response" def analyticsTags = getAnalyticResults(response) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index 3121923d858..478248d3f46 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -55,7 +55,7 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Prebid.Channel -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { @@ -954,7 +954,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_PRICE_FLOOR + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() where: diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy index c1f002a9cb7..01a5a61aca6 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy @@ -19,7 +19,7 @@ import static org.prebid.server.functional.model.request.auction.DsaRequired.NOT import static org.prebid.server.functional.model.request.auction.DsaRequired.REQUIRED import static org.prebid.server.functional.model.request.auction.DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM import static org.prebid.server.functional.model.request.auction.DsaRequired.SUPPORTED -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_DUE_TO_DSA +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_DSA import static org.prebid.server.functional.model.response.auction.DsaAdRender.ADVERTISER_WILL_RENDER import static org.prebid.server.functional.model.response.auction.DsaAdRender.ADVERTISER_WONT_RENDER import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC @@ -316,7 +316,7 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_DSA + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id @@ -496,7 +496,7 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_DSA + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id @@ -536,7 +536,7 @@ class DsaSpec extends PrivacyBaseSpec { def seatNonBid = response.ext.seatnonbid[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_DUE_TO_DSA + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_DSA and: "Response should contain an error" def bidId = bidResponse.seatbid[0].bid[0].id diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 89a6899e8ac..1e0bc5d6c75 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -21,13 +21,13 @@ import java.time.Instant import static org.prebid.server.functional.model.ChannelType.PBJS import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA -import static org.prebid.server.functional.model.pricefloors.Country.CAN -import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.config.Purpose.P1 import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 -import static org.prebid.server.functional.model.config.PurposeEnforcement.* +import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA +import static org.prebid.server.functional.model.pricefloors.Country.CAN +import static org.prebid.server.functional.model.pricefloors.Country.USA import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ACCOUNT_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT @@ -38,7 +38,7 @@ import static org.prebid.server.functional.model.request.auction.ActivityType.TR import static org.prebid.server.functional.model.request.auction.Prebid.Channel import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE -import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REJECTED_BY_PRIVACY +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_PRIVACY import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS @@ -263,7 +263,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def seatNonBid = seatNonBids[0] assert seatNonBid.seat == GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id - assert seatNonBid.nonBid[0].statusCode == REJECTED_BY_PRIVACY + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_PRIVACY and: "seatbid should be empty" assert response.seatbid.isEmpty() diff --git a/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java b/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java index 131a726647f..3c42e27b3ed 100644 --- a/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java +++ b/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java @@ -104,7 +104,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsNotRequiredAndDsaRespons .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -137,7 +137,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsNotRequiredAndDsaRespons .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -333,7 +333,7 @@ public void enforceShouldRejectBidAndAddWarningWhenBidExtHasEmptyDsaAndDsaIsRequ .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -367,7 +367,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndDsaResponseHa .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -401,7 +401,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndDsaResponseHa .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -436,7 +436,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherAndA .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -471,7 +471,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherAndA .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } @Test @@ -505,7 +505,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherNotR .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100)) .build(); assertThat(actual).isEqualTo(expectedParticipation); - verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.REJECTED_BY_DSA_PRIVACY); + verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY); } private static ExtRegs givenExtRegs(DsaRequired dsaRequired, DsaPublisherRender pubRender) { diff --git a/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java b/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java index 49185140d2b..5b9cc66779e 100644 --- a/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java +++ b/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java @@ -22,7 +22,7 @@ public void setUp() { @Test public void succeedShouldRestoreBidderFromRejection() { // given - target.reject("1", BidRejectionReason.OTHER_ERROR); + target.reject("1", BidRejectionReason.ERROR_GENERAL); // when target.succeed("1"); @@ -34,30 +34,30 @@ public void succeedShouldRestoreBidderFromRejection() { @Test public void succeedShouldIgnoreUninvolvedImpIds() { // given - target.reject("1", BidRejectionReason.OTHER_ERROR); + target.reject("1", BidRejectionReason.ERROR_GENERAL); // when target.succeed("2"); // then assertThat(target.getRejectionReasons()) - .isEqualTo(singletonMap("1", BidRejectionReason.OTHER_ERROR)); + .isEqualTo(singletonMap("1", BidRejectionReason.ERROR_GENERAL)); } @Test public void rejectShouldRecordRejectionFirstTimeIfImpIdIsInvolved() { // when - target.reject("1", BidRejectionReason.OTHER_ERROR); + target.reject("1", BidRejectionReason.ERROR_GENERAL); // then assertThat(target.getRejectionReasons()) - .isEqualTo(singletonMap("1", BidRejectionReason.OTHER_ERROR)); + .isEqualTo(singletonMap("1", BidRejectionReason.ERROR_GENERAL)); } @Test public void rejectShouldNotRecordRejectionIfImpIdIsNotInvolved() { // when - target.reject("2", BidRejectionReason.OTHER_ERROR); + target.reject("2", BidRejectionReason.ERROR_GENERAL); // then assertThat(target.getRejectionReasons()).doesNotContainKey("2"); @@ -66,14 +66,14 @@ public void rejectShouldNotRecordRejectionIfImpIdIsNotInvolved() { @Test public void rejectShouldNotRecordRejectionIfImpIdIsAlreadyRejected() { // given - target.reject("1", BidRejectionReason.OTHER_ERROR); + target.reject("1", BidRejectionReason.ERROR_GENERAL); // when - target.reject("1", BidRejectionReason.FAILED_TO_REQUEST_BIDS); + target.reject("1", BidRejectionReason.ERROR_INVALID_BID_RESPONSE); // then assertThat(target.getRejectionReasons()) - .isEqualTo(singletonMap("1", BidRejectionReason.OTHER_ERROR)); + .isEqualTo(singletonMap("1", BidRejectionReason.ERROR_GENERAL)); } @Test @@ -83,14 +83,14 @@ public void rejectAllShouldTryRejectingEachImpId() { target.reject("1", BidRejectionReason.NO_BID); // when - target.rejectAll(BidRejectionReason.TIMED_OUT); + target.rejectAll(BidRejectionReason.ERROR_TIMED_OUT); // then assertThat(target.getRejectionReasons()) .isEqualTo(Map.of( "1", BidRejectionReason.NO_BID, - "2", BidRejectionReason.TIMED_OUT, - "3", BidRejectionReason.TIMED_OUT)); + "2", BidRejectionReason.ERROR_TIMED_OUT, + "3", BidRejectionReason.ERROR_TIMED_OUT)); } @Test diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java index 5a677170187..83079b4d825 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java @@ -50,6 +50,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -60,6 +61,7 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.given; @@ -147,6 +149,8 @@ public void shouldReturnFailedToRequestBidsErrorWhenBidderReturnsEmptyHttpReques assertThat(bidderSeatBid.getErrors()) .containsOnly(BidderError.failedToRequestBids( "The bidder failed to generate any bid requests, but also failed to generate an error")); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -177,6 +181,8 @@ public void shouldTolerateBidderReturningErrorsAndNoHttpRequests() { assertThat(bidderSeatBid.getHttpCalls()).isEmpty(); assertThat(bidderSeatBid.getErrors()) .extracting(BidderError::getMessage).containsOnly("error1", "error2"); + + verifyNoInteractions(bidRejectionTracker); } @Test @@ -219,6 +225,9 @@ public void shouldPassStoredResponseToBidderMakeBidsMethodAndReturnSeatBids() { .extracting(HttpResponse::getBody) .isEqualTo("storedResponse"); assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -255,6 +264,9 @@ public void shouldMakeRequestToBidderWhenStoredResponseDefinedButBidderCreatesMo // then verify(httpClient, times(2)).request(any(), anyString(), any(), any(byte[].class), anyLong()); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -287,6 +299,9 @@ public void shouldSendPopulatedGetRequestWithoutBody() { // then verify(httpClient).request(any(), anyString(), any(), (byte[]) isNull(), anyLong()); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -320,6 +335,9 @@ public void shouldSendMultipleRequests() throws JsonProcessingException { // then verify(httpClient, times(2)).request(any(), anyString(), any(), any(byte[].class), anyLong()); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -350,6 +368,9 @@ public void shouldReturnBidsCreatedByBidder() { // then assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -381,6 +402,9 @@ public void shouldReturnBidsCreatedByMakeBids() { // then assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -417,6 +441,9 @@ public void shouldReturnFledgeCreatedByBidder() { // then assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids); assertThat(bidderSeatBid.getFledgeAuctionConfigs()).hasSameElementsAs(fledgeAuctionConfigs); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -450,6 +477,9 @@ public void shouldCompressRequestBodyIfContentEncodingHeaderIsGzip() { final ArgumentCaptor actualRequestBody = ArgumentCaptor.forClass(byte[].class); verify(httpClient).request(any(), anyString(), any(), actualRequestBody.capture(), anyLong()); assertThat(actualRequestBody.getValue()).isNotSameAs(EMPTY_BYTE_BODY); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -544,6 +574,9 @@ public void processBids(List bids) { verify(bidder, times(2)).makeBidderResponse(any(), any()); assertThat(bidderSeatBid.getBids()).containsOnly(bidderBidDeal1, bidderBidDeal2); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -590,6 +623,9 @@ public void shouldFinishWhenAllDealRequestsAreFinishedAndNoDealsProvided() { verify(bidder, times(4)).makeBidderResponse(any(), any()); assertThat(bidderSeatBid.getBids()).contains(bidderBid, bidderBid, bidderBid, bidderBid); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -654,6 +690,9 @@ public void shouldReturnFullDebugInfoIfDebugEnabled() throws JsonProcessingExcep .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) .status(200) .build()); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -717,8 +756,8 @@ public void shouldReturnRecordBidRejections() throws JsonProcessingException { // then verify(bidRejectionTracker, atLeast(1)).succeed(secondRequestBids); - verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); - verify(bidRejectionTracker).reject(singleton("3"), BidRejectionReason.OTHER_ERROR); + verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.REQUEST_BLOCKED_GENERAL); + verify(bidRejectionTracker).reject(singleton("3"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); } @Test @@ -761,6 +800,9 @@ public void shouldNotReturnSensitiveHeadersInFullDebugInfo() assertThat(bidderSeatBid.getHttpCalls()) .extracting(ExtHttpCall::getRequestheaders) .containsExactly(singletonMap("headerKey", singletonList("headerValue"))); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } @Test @@ -775,7 +817,9 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndGlobalTimeoutAlreadyExp .uri("uri1") .headers(headers) .payload(givenBidRequest) - .body(requestBody))), + .body(requestBody) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId) + .collect(Collectors.toSet())))), emptyList())); given(requestEnricher.enrichHeaders(anyString(), any(), any(), any(), any())).willReturn(headers); @@ -804,6 +848,8 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndGlobalTimeoutAlreadyExp .requestbody(mapper.writeValueAsString(givenBidRequest)) .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) .build()); + + verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT); } @Test @@ -817,6 +863,7 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() throw .uri("uri1") .headers(headers) .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) .body(requestBody))), emptyList())); @@ -849,6 +896,8 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() throw .requestbody(mapper.writeValueAsString(givenBidRequest)) .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) .build()); + + verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_GENERAL); } @Test @@ -862,6 +911,7 @@ public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() throws JsonP .uri("uri1") .headers(headers) .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) .body(requestBody))), emptyList())); @@ -899,15 +949,75 @@ public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() throws JsonP assertThat(bidderSeatBid.getErrors()) .extracting(BidderError::getMessage) .containsExactly("Unexpected status code: 500. Run with request.test = 1 for more info"); + + verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); } @Test - public void shouldTolerateAlreadyExpiredGlobalTimeout() { + public void shouldReturnFullDebugInfoIfDebugEnabledAndBidderIsUnreachable() throws JsonProcessingException { // given - given(bidder.makeHttpRequests(any())).willReturn(Result.of( - singletonList(givenSimpleHttpRequest(identity())), + final MultiMap headers = MultiMap.caseInsensitiveMultiMap().add("headerKey", "headerValue"); + final BidRequest givenBidRequest = givenBidRequest(identity()); + final byte[] requestBody = mapper.writeValueAsBytes(givenBidRequest); + given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( + givenSimpleHttpRequest(httpRequestBuilder -> httpRequestBuilder + .uri("uri1") + .headers(headers) + .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) + .body(requestBody))), emptyList())); + given(requestEnricher.enrichHeaders(anyString(), any(), any(), any(), any())).willReturn(headers); + + givenHttpClientReturnsResponses(HttpClientResponse.of(503, null, "responseBody1")); + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidder("bidder") + .bidRequest(BidRequest.builder().build()) + .build(); + + // when + final BidderSeatBid bidderSeatBid = + target + .requestBids( + bidder, + bidderRequest, + bidRejectionTracker, + timeout, + CaseInsensitiveMultiMap.empty(), + bidderAliases, + true) + .result(); + + // then + assertThat(bidderSeatBid.getHttpCalls()).containsExactly( + ExtHttpCall.builder() + .uri("uri1") + .requestbody(mapper.writeValueAsString(givenBidRequest)) + .responsebody("responseBody1") + .requestheaders(singletonMap("headerKey", singletonList("headerValue"))) + .status(503).build()); + + assertThat(bidderSeatBid.getErrors()) + .extracting(BidderError::getMessage) + .containsExactly("Unexpected status code: 503. Run with request.test = 1 for more info"); + + verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + } + + @Test + public void shouldTolerateAlreadyExpiredGlobalTimeout() throws JsonProcessingException { + // given + final BidRequest givenBidRequest = givenBidRequest(identity()); + final byte[] requestBody = mapper.writeValueAsBytes(givenBidRequest); + given(bidder.makeHttpRequests(any())).willReturn(Result.of(singletonList( + givenSimpleHttpRequest(httpRequestBuilder -> httpRequestBuilder + .uri("uri1") + .payload(givenBidRequest) + .impIds(givenBidRequest.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) + .body(requestBody))), + emptyList())); final BidderRequest bidderRequest = BidderRequest.builder() .bidder("bidder") .bidRequest(BidRequest.builder().build()) @@ -929,6 +1039,8 @@ public void shouldTolerateAlreadyExpiredGlobalTimeout() { .extracting(BidderError::getMessage) .containsOnly("Timeout has been exceeded"); verifyNoInteractions(httpClient); + + verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT); } @Test @@ -960,7 +1072,6 @@ public void shouldNotifyBidderOfTimeout() { // then verify(bidderErrorNotifier).processTimeout(any(), same(bidder)); - verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.TIMED_OUT); } @Test @@ -968,17 +1079,19 @@ public void shouldTolerateMultipleErrors() { // given given(bidder.makeHttpRequests(any())).willReturn(Result.of(asList( // this request will fail with response exception - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("1"))), // this request will fail with timeout - givenSimpleHttpRequest(identity()), - // this request will fail with 500 status - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("2"))), + // this request will fail with 503 status + givenSimpleHttpRequest(builder -> builder.impIds(singleton("3"))), // this request will fail with 400 status - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("4"))), + // this request will fail with 404 status + givenSimpleHttpRequest(builder -> builder.impIds(singleton("5"))), // this request will get 204 status - givenSimpleHttpRequest(identity()), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("6"))), // finally this request will succeed - givenSimpleHttpRequest(identity())), + givenSimpleHttpRequest(builder -> builder.impIds(singleton("7")))), singletonList(BidderError.badInput("makeHttpRequestsError")))); when(requestEnricher.enrichHeaders(anyString(), any(), any(), any(), any())) .thenAnswer(invocation -> MultiMap.caseInsensitiveMultiMap()); @@ -987,10 +1100,12 @@ public void shouldTolerateMultipleErrors() { .willReturn(Future.failedFuture(new RuntimeException("Response exception"))) // simulate timeout for the second request .willReturn(Future.failedFuture(new TimeoutException("Timeout exception"))) - // simulate 500 status - .willReturn(Future.succeededFuture(HttpClientResponse.of(500, null, EMPTY))) + // simulate 503 status + .willReturn(Future.succeededFuture(HttpClientResponse.of(503, null, EMPTY))) // simulate 400 status .willReturn(Future.succeededFuture(HttpClientResponse.of(400, null, EMPTY))) + // simulate 400 status + .willReturn(Future.succeededFuture(HttpClientResponse.of(404, null, EMPTY))) // simulate 204 status .willReturn(Future.succeededFuture(HttpClientResponse.of(204, null, EMPTY))) // simulate 200 status @@ -1027,9 +1142,19 @@ public void shouldTolerateMultipleErrors() { BidderError.badInput("makeHttpRequestsError"), BidderError.generic("Response exception"), BidderError.timeout("Timeout exception"), - BidderError.badServerResponse("Unexpected status code: 500. Run with request.test = 1 for more info"), + BidderError.badServerResponse("Unexpected status code: 503. Run with request.test = 1 for more info"), BidderError.badInput("Unexpected status code: 400. Run with request.test = 1 for more info"), + BidderError.badServerResponse("Unexpected status code: 404. Run with request.test = 1 for more info"), BidderError.badServerResponse("makeBidsError")); + + verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.ERROR_GENERAL); + verify(bidRejectionTracker).reject(singleton("2"), BidRejectionReason.ERROR_TIMED_OUT); + verify(bidRejectionTracker).reject(singleton("3"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + verify(bidRejectionTracker).reject(singleton("4"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + verify(bidRejectionTracker).reject(singleton("5"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE); + verify(bidRejectionTracker, never()).reject(eq(singleton("6")), any()); + verify(bidRejectionTracker, never()).reject(eq(singleton("7")), any()); + } @Test @@ -1060,6 +1185,9 @@ public void shouldNotMakeBidsIfResponseStatusIs204() { // then verify(bidder, never()).makeBidderResponse(any(), any()); verify(bidder, never()).makeBids(any(), any()); + + verify(bidRejectionTracker, never()).reject(anyString(), any()); + verify(bidRejectionTracker, never()).reject(anyList(), any()); } private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java index 38746f2b799..61ecb9825ac 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java @@ -347,7 +347,7 @@ public void shouldRejectBidsHavingPriceBelowFloor() { // then verify(priceFloorAdjuster, times(2)).revertAdjustmentForImp(any(), any(), any(), any()); - verify(rejectionTracker).reject("impId1", BidRejectionReason.REJECTED_DUE_TO_PRICE_FLOOR); + verify(rejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); assertThat(singleton(result)) .extracting(AuctionParticipation::getBidderResponse) .extracting(BidderResponse::getSeatBid) diff --git a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java index ead616d13b7..2d6d1db8377 100644 --- a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java @@ -14,6 +14,8 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; @@ -24,6 +26,7 @@ import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -34,6 +37,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.prebid.server.settings.model.BidValidationEnforcement.enforce; import static org.prebid.server.settings.model.BidValidationEnforcement.skip; import static org.prebid.server.settings.model.BidValidationEnforcement.warn; @@ -47,6 +51,9 @@ public class ResponseBidValidatorTest extends VertxTest { @Mock private Metrics metrics; + @Mock + private BidRejectionTracker bidRejectionTracker; + private ResponseBidValidator target; @Mock(strictness = LENIENT) @@ -70,6 +77,7 @@ public void validateShouldFailedIfBidderBidCurrencyIsIncorrect() { // then assertThat(result.getErrors()).containsOnly("BidResponse currency \"invalid\" is not valid"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -80,6 +88,7 @@ public void validateShouldFailIfMissingBid() { // then assertThat(result.getErrors()).containsOnly("Empty bid object submitted"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -90,6 +99,7 @@ public void validateShouldFailIfBidHasNoId() { // then assertThat(result.getErrors()).containsOnly("Bid missing required field 'id'"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -100,6 +110,7 @@ public void validateShouldFailIfBidHasNoImpId() { // then assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing required field 'impid'"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -113,6 +124,7 @@ public void validateShouldSuccessForDealZeroPriceBid() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -123,6 +135,7 @@ public void validateShouldFailIfBidHasNoCrid() { // then assertThat(result.getErrors()).containsOnly("Bid \"bidId1\" missing creative ID"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -137,6 +150,8 @@ public void validateShouldFailIfBannerBidHasNoWidthAndHeight() { BidResponse validation `enforce`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='nullxnull'"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -151,6 +166,8 @@ public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() { BidResponse validation `enforce`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='150x150'"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -168,6 +185,8 @@ public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() { BidResponse validation `enforce`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='50x250'"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -181,6 +200,7 @@ public void validateShouldReturnSuccessIfNonBannerBidHasAnySize() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -198,6 +218,7 @@ public void validateShouldTolerateMissingImpExtBidderNode() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -214,6 +235,7 @@ public void validateShouldReturnSuccessIfBannerBidHasInvalidSizeButAccountDoesNo // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -228,6 +250,7 @@ public void validateShouldFailIfBidHasNoCorrespondingImp() { // then assertThat(result.getErrors()) .containsOnly("Bid \"bidId1\" has no corresponding imp in request"); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -245,6 +268,8 @@ public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() BidResponse validation `enforce`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=http://site.com/creative.jpg"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -262,6 +287,8 @@ public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureCon BidResponse validation `enforce`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=http%3A//site.com/creative.jpg"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -279,6 +306,8 @@ public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() BidResponse validation `enforce`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=//site.com/creative.jpg"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -292,6 +321,7 @@ public void validateShouldReturnSuccessIfBidHasInsecureCreativeInInsecureContext // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -307,6 +337,7 @@ public void validateShouldFailedIfVideoBidHasNoNurlAndAdm() { assertThat(result.getErrors()) .containsOnly("Bid \"bidId1\" with video type missing adm and nurl"); verify(metrics).updateAdapterRequestErrorMetric(BIDDER_NAME, MetricName.badserverresponse); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -320,6 +351,7 @@ public void validateShouldReturnSuccessfulResultForValidVideoBidWithNurl() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -333,6 +365,7 @@ public void validateShouldReturnSuccessfulResultForValidVideoBidWithAdm() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -346,6 +379,7 @@ public void validateShouldReturnSuccessfulResultForValidBid() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -362,6 +396,7 @@ public void validateShouldReturnSuccessIfBannerSizeValidationNotEnabled() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -383,6 +418,8 @@ public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn( BidResponse validation `warn`: bidder `bidder` response triggers \ creative size validation for bid bidId1, account=account, referrer=unknown, \ max imp size='100x200', bid response size='nullxnull'"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -399,6 +436,7 @@ public void validateShouldReturnSuccessIfSecureMarkupValidationNotEnabled() { // then assertThat(result.hasErrors()).isFalse(); + verifyNoInteractions(bidRejectionTracker); } @Test @@ -420,6 +458,8 @@ public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWar BidResponse validation `warn`: bidder `bidder` response triggers \ secure creative validation for bid bidId1, account=account, referrer=unknown, \ adm=http://site.com/creative.jpg"""); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -433,6 +473,8 @@ public void validateShouldIncrementSizeValidationErrMetrics() { // then verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -449,6 +491,8 @@ public void validateShouldIncrementSizeValidationWarnMetrics() { // then verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } @Test @@ -462,6 +506,8 @@ public void validateShouldIncrementSecureValidationErrMetrics() { // then verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } @Test @@ -478,6 +524,8 @@ public void validateShouldIncrementSecureValidationWarnMetrics() { // then verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn); + verify(bidRejectionTracker) + .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } private BidRequest givenRequest(UnaryOperator impCustomizer) { @@ -522,22 +570,23 @@ private static BidderBid givenBid(BidType type, String bidCurrency, UnaryOperato return BidderBid.of(bidCustomizer.apply(bidBuilder).build(), type, bidCurrency); } - private static AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { + private AuctionContext givenAuctionContext(BidRequest bidRequest, Account account) { return AuctionContext.builder() + .bidRejectionTrackers(Map.of("bidder", bidRejectionTracker)) .account(account) .bidRequest(bidRequest) .build(); } - private static AuctionContext givenAuctionContext(BidRequest bidRequest) { + private AuctionContext givenAuctionContext(BidRequest bidRequest) { return givenAuctionContext(bidRequest, givenAccount()); } - private static AuctionContext givenAuctionContext(Account account) { + private AuctionContext givenAuctionContext(Account account) { return givenAuctionContext(givenBidRequest(identity()), account); } - private static AuctionContext givenAuctionContext() { + private AuctionContext givenAuctionContext() { return givenAuctionContext(givenBidRequest(identity()), givenAccount()); } From ef313c085e5ce8b1936f35497a422bfff39eeabc Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Mon, 12 Aug 2024 18:03:20 +0200 Subject: [PATCH 011/170] Core: Fix naming for storage service (#3374) --- .../server/cache/BasicPbcStorageService.java | 16 ++++++------- .../server/cache/PbcStorageService.java | 8 +++---- .../cache/BasicPbcStorageServiceTest.java | 24 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java index 6db93819349..956330cd1ca 100644 --- a/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java +++ b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java @@ -47,17 +47,17 @@ public Future storeEntry(String key, StorageDataType type, Integer ttlseconds, String application, - String moduleCode) { + String appCode) { try { - validateStoreData(key, value, application, type, moduleCode); + validateStoreData(key, value, application, type, appCode); } catch (PreBidException e) { return Future.failedFuture(e); } final ModuleCacheRequest moduleCacheRequest = ModuleCacheRequest.of( - constructEntryKey(key, moduleCode), + constructEntryKey(key, appCode), type, prepareValueForStoring(value, type), application, @@ -124,18 +124,18 @@ private Future processStoreResponse(int statusCode, String responseBody) { } @Override - public Future retrieveModuleEntry(String key, - String moduleCode, - String application) { + public Future retrieveEntry(String key, + String appCode, + String application) { try { - validateRetrieveData(key, application, moduleCode); + validateRetrieveData(key, application, appCode); } catch (PreBidException e) { return Future.failedFuture(e); } return httpClient.get( - getRetrieveEndpoint(key, moduleCode, application), + getRetrieveEndpoint(key, appCode, application), securedCallHeaders(), callTimeoutMs) .map(response -> toModuleCacheResponse(response.getStatusCode(), response.getBody())); diff --git a/src/main/java/org/prebid/server/cache/PbcStorageService.java b/src/main/java/org/prebid/server/cache/PbcStorageService.java index 4e8d432a158..c76f0884d52 100644 --- a/src/main/java/org/prebid/server/cache/PbcStorageService.java +++ b/src/main/java/org/prebid/server/cache/PbcStorageService.java @@ -11,9 +11,9 @@ Future storeEntry(String key, StorageDataType type, Integer ttlseconds, String application, - String moduleCode); + String appCode); - Future retrieveModuleEntry(String key, String moduleCode, String application); + Future retrieveEntry(String key, String appCode, String application); static NoOpPbcStorageService noOp() { return new NoOpPbcStorageService(); @@ -27,13 +27,13 @@ public Future storeEntry(String key, StorageDataType type, Integer ttlseconds, String application, - String moduleCode) { + String appCode) { return Future.succeededFuture(); } @Override - public Future retrieveModuleEntry(String key, String moduleCode, String application) { + public Future retrieveEntry(String key, String appCode, String application) { return Future.succeededFuture(ModuleCacheResponse.empty()); } } diff --git a/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java b/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java index 3a146a26dc4..c7b09518ae7 100644 --- a/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java +++ b/src/test/java/org/prebid/server/cache/BasicPbcStorageServiceTest.java @@ -225,10 +225,10 @@ public void storeEntryShouldCreateCallWithApiKeyInHeader() { } @Test - public void retrieveModuleEntryShouldReturnFailedFutureIfModuleKeyIsMissed() { + public void retrieveModuleEntryShouldReturnFailedFutureIfKeyIsMissed() { // when final Future result = - target.retrieveModuleEntry(null, "some-module-code", "some-app"); + target.retrieveEntry(null, "some-module-code", "some-app"); // then assertThat(result.failed()).isTrue(); @@ -237,10 +237,10 @@ public void retrieveModuleEntryShouldReturnFailedFutureIfModuleKeyIsMissed() { } @Test - public void retrieveModuleEntryShouldReturnFailedFutureIfModuleApplicationIsMissed() { + public void retrieveModuleEntryShouldReturnFailedFutureIfApplicationIsMissed() { // when final Future result = - target.retrieveModuleEntry("some-key", "some-module-code", null); + target.retrieveEntry("some-key", "some-module-code", null); // then assertThat(result.failed()).isTrue(); @@ -249,10 +249,10 @@ public void retrieveModuleEntryShouldReturnFailedFutureIfModuleApplicationIsMiss } @Test - public void retrieveModuleEntryShouldReturnFailedFutureIfModuleCodeIsMissed() { + public void retrieveModuleEntryShouldReturnFailedFutureIfCodeIsMissed() { // when final Future result = - target.retrieveModuleEntry("some-key", null, "some-app"); + target.retrieveEntry("some-key", null, "some-app"); // then assertThat(result.failed()).isTrue(); @@ -261,9 +261,9 @@ public void retrieveModuleEntryShouldReturnFailedFutureIfModuleCodeIsMissed() { } @Test - public void retrieveModuleEntryShouldCreateCallWithApiKeyInHeader() { + public void retrieveEntryShouldCreateCallWithApiKeyInHeader() { // when - target.retrieveModuleEntry("some-key", "some-module-code", "some-app"); + target.retrieveEntry("some-key", "some-module-code", "some-app"); // then final MultiMap result = captureRetrieveRequestHeaders(); @@ -271,9 +271,9 @@ public void retrieveModuleEntryShouldCreateCallWithApiKeyInHeader() { } @Test - public void retrieveModuleEntryShouldCreateCallWithKeyInParams() { + public void retrieveEntryShouldCreateCallWithKeyInParams() { // when - target.retrieveModuleEntry("some-key", "some-module-code", "some-app"); + target.retrieveEntry("some-key", "some-module-code", "some-app"); // then final String result = captureRetrieveUrl(); @@ -282,10 +282,10 @@ public void retrieveModuleEntryShouldCreateCallWithKeyInParams() { } @Test - public void retrieveModuleEntryShouldReturnExpectedResponse() { + public void retrieveEntryShouldReturnExpectedResponse() { // when final Future result = - target.retrieveModuleEntry("some-key", "some-module-code", "some-app"); + target.retrieveEntry("some-key", "some-module-code", "some-app"); // then assertThat(result.result()) From 8609e25c5d20bd463d1821597dee76b48b6a64c0 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:01:04 +0200 Subject: [PATCH 012/170] Core: Support Bidder Specific Imp Level Params (#3254) --- .../server/auction/ExchangeService.java | 13 +- .../prebid/server/auction/ImpAdjuster.java | 99 + .../openrtb/ext/request/ExtImpPrebid.java | 5 + .../spring/config/ServiceConfiguration.java | 21 +- .../server/validation/ImpValidator.java | 655 ++++ .../server/validation/RequestValidator.java | 599 +--- .../validation/ValidationException.java | 6 +- .../functional/model/bidder/BidderName.groovy | 1 + .../model/bidderspecific/BidderImp.groovy | 2 +- .../model/request/auction/Deal.groovy | 9 +- .../model/request/auction/DealExt.groovy | 2 + .../model/request/auction/DealLineItem.groovy | 2 + .../model/request/auction/Format.groovy | 2 + .../model/request/auction/ImpExtPrebid.groovy | 2 + .../model/request/auction/Pmp.groovy | 6 + .../functional/tests/BidderFormatSpec.groovy | 4 +- .../functional/tests/ImpRequestSpec.groovy | 229 ++ .../server/auction/ExchangeServiceTest.java | 7 +- .../server/auction/ImpAdjusterTest.java | 278 ++ .../server/validation/ImpValidatorTest.java | 2379 ++++++++++++++ .../validation/RequestValidatorTest.java | 2806 +++-------------- .../test-auction-generic-request.json | 16 + .../test-generic-bid-request.json | 36 +- 23 files changed, 4270 insertions(+), 2909 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/ImpAdjuster.java create mode 100644 src/main/java/org/prebid/server/validation/ImpValidator.java create mode 100644 src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy create mode 100644 src/test/java/org/prebid/server/auction/ImpAdjusterTest.java create mode 100644 src/test/java/org/prebid/server/validation/ImpValidatorTest.java diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index bdade14ab8a..d97d0d582ab 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -150,9 +150,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -/** - * Executes an OpenRTB v2.5-2.6 Auction. - */ public class ExchangeService { private static final Logger logger = LoggerFactory.getLogger(ExchangeService.class); @@ -174,6 +171,7 @@ public class ExchangeService { private final StoredResponseProcessor storedResponseProcessor; private final PrivacyEnforcementService privacyEnforcementService; private final FpdResolver fpdResolver; + private final ImpAdjuster impAdjuster; private final SupplyChainResolver supplyChainResolver; private final DebugResolver debugResolver; private final MediaTypeProcessor mediaTypeProcessor; @@ -204,6 +202,7 @@ public ExchangeService(double logSamplingRate, StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, MediaTypeProcessor mediaTypeProcessor, @@ -234,6 +233,7 @@ public ExchangeService(double logSamplingRate, this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); this.privacyEnforcementService = Objects.requireNonNull(privacyEnforcementService); this.fpdResolver = Objects.requireNonNull(fpdResolver); + this.impAdjuster = Objects.requireNonNull(impAdjuster); this.supplyChainResolver = Objects.requireNonNull(supplyChainResolver); this.debugResolver = Objects.requireNonNull(debugResolver); this.mediaTypeProcessor = Objects.requireNonNull(mediaTypeProcessor); @@ -299,7 +299,6 @@ private Future runAuction(AuctionContext receivedContext) { .map(receivedContext::with)) .map(context -> updateRequestMetric(context, uidsCookie, aliases, account, requestTypeMetric)) - .compose(context -> CompositeFuture.join( context.getAuctionParticipations().stream() .map(auctionParticipation -> processAndRequestBids( @@ -528,7 +527,6 @@ private Future> extractAuctionParticipations( .toList(); final Map> impBidderToStoredBidResponse = storedResponseResult.getImpBidderToStoredBidResponse(); - return makeAuctionParticipation( bidders, context, @@ -852,6 +850,7 @@ private AuctionParticipation createAuctionParticipation( bidderToMultiBid, biddersToConfigs, bidderToPrebidBidders, + bidderAliases, context); final BidderRequest bidderRequest = BidderRequest.builder() @@ -878,6 +877,7 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, Map bidderToMultiBid, Map biddersToConfigs, Map bidderToPrebidBidders, + BidderAliases bidderAliases, AuctionContext context) { final String bidder = bidderPrivacyResult.getRequestBidder(); @@ -938,6 +938,7 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, transmitTid, useFirstPartyData, context.getAccount(), + bidderAliases, context.getDebugWarnings()); return bidRequest.toBuilder() @@ -975,10 +976,12 @@ private List prepareImps(String bidder, boolean transmitTid, boolean useFirstPartyData, Account account, + BidderAliases bidderAliases, List debugWarnings) { return bidRequest.getImp().stream() .filter(imp -> bidderParamsFromImpExt(imp.getExt()).hasNonNull(bidder)) + .map(imp -> impAdjuster.adjust(imp, bidder, bidderAliases, debugWarnings)) .map(imp -> prepareImp(imp, bidder, bidRequest, transmitTid, useFirstPartyData, account, debugWarnings)) .toList(); } diff --git a/src/main/java/org/prebid/server/auction/ImpAdjuster.java b/src/main/java/org/prebid/server/auction/ImpAdjuster.java new file mode 100644 index 00000000000..32ecff524c5 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/ImpAdjuster.java @@ -0,0 +1,99 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.validation.ImpValidator; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class ImpAdjuster { + + private static final String IMP_EXT = "ext"; + private static final String EXT_PREBID = "prebid"; + private static final String EXT_PREBID_BIDDER = "bidder"; + private static final String EXT_PREBID_IMP = "imp"; + + private final ImpValidator impValidator; + private final JacksonMapper jacksonMapper; + private final JsonMerger jsonMerger; + + public ImpAdjuster(JacksonMapper jacksonMapper, + JsonMerger jsonMerger, + ImpValidator impValidator) { + + this.impValidator = Objects.requireNonNull(impValidator); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public Imp adjust(Imp originalImp, String bidder, BidderAliases bidderAliases, List debugMessages) { + final JsonNode impExtPrebidImp = bidderParamsFromImpExtPrebidImp(originalImp.getExt()); + if (impExtPrebidImp == null) { + return originalImp; + } + + final JsonNode bidderNode = getBidderNode(bidder, bidderAliases, impExtPrebidImp); + + if (bidderNode == null || bidderNode.isEmpty()) { + return originalImp; + } + + // remove circular references according to the requirements + removeExtPrebidBidder(bidderNode); + + try { + final JsonNode originalImpNode = jacksonMapper.mapper().valueToTree(originalImp); + final JsonNode mergedImpNode = jsonMerger.merge(bidderNode, originalImpNode); + + // clean up merged imp.ext.prebid.imp + removeImpExtPrebidImp(mergedImpNode); + + final Imp resultImp = jacksonMapper.mapper().convertValue(mergedImpNode, Imp.class); + + impValidator.validateImp(resultImp); + return resultImp; + } catch (Exception e) { + debugMessages.add("imp.ext.prebid.imp.%s can not be merged into original imp [id=%s], reason: %s" + .formatted(bidder, originalImp.getId(), e.getMessage())); + return originalImp; + } + } + + private static JsonNode bidderParamsFromImpExtPrebidImp(ObjectNode ext) { + return Optional.ofNullable(ext) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(prebidNode -> prebidNode.get(EXT_PREBID_IMP)) + .orElse(null); + } + + private static JsonNode getBidderNode(String bidderName, BidderAliases bidderAliases, JsonNode node) { + final Iterator fieldNames = node.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (bidderAliases.isSame(fieldName, bidderName)) { + return node.get(fieldName); + } + } + return null; + } + + private static void removeExtPrebidBidder(JsonNode bidderNode) { + Optional.ofNullable(bidderNode.get(IMP_EXT)) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .ifPresent(ext -> ext.remove(EXT_PREBID_BIDDER)); + } + + private static void removeImpExtPrebidImp(JsonNode mergedImpNode) { + Optional.ofNullable(mergedImpNode.get(IMP_EXT)) + .map(extNode -> extNode.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .ifPresent(prebid -> prebid.remove(EXT_PREBID_IMP)); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java index f80f925cd5f..fd6206cb90d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpPrebid.java @@ -56,4 +56,9 @@ public class ExtImpPrebid { * Defines the contract for bidrequest.imp[i].ext.prebid.passthrough */ JsonNode passthrough; + + /** + * Defines the contract for bidrequest.imp[i].ext.prebid.imp + */ + ObjectNode imp; } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 71e55bbc780..8e21fe77b4b 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -18,6 +18,7 @@ import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.FpdResolver; import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.ImpAdjuster; import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.InterstitialProcessor; import org.prebid.server.auction.IpAddressHelper; @@ -111,6 +112,7 @@ import org.prebid.server.util.VersionInfo; import org.prebid.server.util.system.CpuLoadAverageStats; import org.prebid.server.validation.BidderParamValidator; +import org.prebid.server.validation.ImpValidator; import org.prebid.server.validation.RequestValidator; import org.prebid.server.validation.ResponseBidValidator; import org.prebid.server.validation.VideoRequestValidator; @@ -245,6 +247,11 @@ FpdResolver fpdResolver(JacksonMapper mapper, JsonMerger jsonMerger) { return new FpdResolver(mapper, jsonMerger); } + @Bean + ImpAdjuster impAdjuster(ImpValidator impValidator, JacksonMapper jacksonMapper, JsonMerger jsonMerger) { + return new ImpAdjuster(jacksonMapper, jsonMerger, impValidator); + } + @Bean OrtbTypesResolver ortbTypesResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { return new OrtbTypesResolver(logSamplingRate, jacksonMapper, jsonMerger); @@ -819,6 +826,7 @@ ExchangeService exchangeService( StoredResponseProcessor storedResponseProcessor, PrivacyEnforcementService privacyEnforcementService, FpdResolver fpdResolver, + ImpAdjuster impAdjuster, SupplyChainResolver supplyChainResolver, DebugResolver debugResolver, CompositeMediaTypeProcessor mediaTypeProcessor, @@ -850,6 +858,7 @@ ExchangeService exchangeService( storedResponseProcessor, privacyEnforcementService, fpdResolver, + impAdjuster, supplyChainResolver, debugResolver, mediaTypeProcessor, @@ -989,10 +998,18 @@ VersionInfo versionInfo(JacksonMapper jacksonMapper) { return VersionInfo.create("git-revision.json", jacksonMapper); } + @Bean + ImpValidator impValidator(BidderParamValidator bidderParamValidator, + BidderCatalog bidderCatalog, + JacksonMapper mapper) { + + return new ImpValidator(bidderParamValidator, bidderCatalog, mapper); + } + @Bean RequestValidator requestValidator( BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, @Value("${logging.sampling-rate:0.01}") double logSamplingRate, @@ -1000,7 +1017,7 @@ RequestValidator requestValidator( return new RequestValidator( bidderCatalog, - bidderParamValidator, + impValidator, metrics, mapper, logSamplingRate, diff --git a/src/main/java/org/prebid/server/validation/ImpValidator.java b/src/main/java/org/prebid/server/validation/ImpValidator.java new file mode 100644 index 00000000000..f90df916af1 --- /dev/null +++ b/src/main/java/org/prebid/server/validation/ImpValidator.java @@ -0,0 +1,655 @@ +package org.prebid.server.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Asset; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.DataObject; +import com.iab.openrtb.request.EventTracker; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.ImageObject; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Metric; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Request; +import com.iab.openrtb.request.TitleObject; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.request.VideoObject; +import com.iab.openrtb.request.ntv.ContextSubType; +import com.iab.openrtb.request.ntv.ContextType; +import com.iab.openrtb.request.ntv.DataAssetType; +import com.iab.openrtb.request.ntv.EventTrackingMethod; +import com.iab.openrtb.request.ntv.EventType; +import com.iab.openrtb.request.ntv.PlacementType; +import com.iab.openrtb.request.ntv.Protocol; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; +import org.prebid.server.util.StreamUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +public class ImpValidator { + + private static final String PREBID_EXT = "prebid"; + private static final String BIDDER_EXT = "bidder"; + private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500; + + private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; + private static final String IMP_EXT = "imp"; + + private final BidderParamValidator bidderParamValidator; + private final BidderCatalog bidderCatalog; + private final JacksonMapper mapper; + + public ImpValidator(BidderParamValidator bidderParamValidator, BidderCatalog bidderCatalog, JacksonMapper mapper) { + this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + this.mapper = Objects.requireNonNull(mapper); + } + + public void validateImps(List imps, + Map aliases, + List warnings) throws ValidationException { + + for (int i = 0; i < imps.size(); i++) { + final Imp imp = imps.get(i); + validateImp(imp, "request.imp[%d]".formatted(i)); + fillAndValidateNative(imp.getXNative(), i); + validateImpExt(imp.getExt(), aliases, i, warnings); + } + } + + public void validateImp(Imp imp) throws ValidationException { + validateImp(imp, "imp[id=%s]".formatted(imp.getId())); + } + + private void validateImp(Imp imp, String msgPrefix) throws ValidationException { + if (StringUtils.isBlank(imp.getId())) { + throw new ValidationException("%s missing required field: \"id\"", msgPrefix); + } + if (imp.getMetric() != null && !imp.getMetric().isEmpty()) { + validateMetrics(imp.getMetric(), msgPrefix); + } + if (imp.getBanner() == null && imp.getVideo() == null && imp.getAudio() == null && imp.getXNative() == null) { + throw new ValidationException( + "%s must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", + msgPrefix); + } + + final boolean isInterstitialImp = Objects.equals(imp.getInstl(), 1); + validateBanner(imp.getBanner(), isInterstitialImp, msgPrefix); + validateVideoMimes(imp.getVideo(), msgPrefix); + validateAudioMimes(imp.getAudio(), msgPrefix); + validatePmp(imp.getPmp(), msgPrefix); + } + + private void fillAndValidateNative(Native xNative, int impIndex) throws ValidationException { + if (xNative == null) { + return; + } + + final Request nativeRequest = parseNativeRequest(xNative.getRequest(), impIndex); + + validateNativeContextTypes(nativeRequest.getContext(), nativeRequest.getContextsubtype(), impIndex); + validateNativePlacementType(nativeRequest.getPlcmttype(), impIndex); + final List updatedAssets = validateAndGetUpdatedNativeAssets(nativeRequest.getAssets(), impIndex); + validateNativeEventTrackers(nativeRequest.getEventtrackers(), impIndex); + + // modifier was added to reduce memory consumption on updating bidRequest.imp[i].native.request object + xNative.setRequest(toEncodedRequest(nativeRequest, updatedAssets)); + } + + private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { + if (StringUtils.isBlank(rawStringNativeRequest)) { + throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); + } + try { + return mapper.mapper().readValue(rawStringNativeRequest, Request.class); + } catch (IOException e) { + throw new ValidationException("Error while parsing request.imp[%d].native.request: %s", + impIndex, + ExceptionUtils.getMessage(e)); + } + } + + private void validateNativeContextTypes(Integer context, Integer contextSubType, int index) + throws ValidationException { + + final int type = context != null ? context : 0; + if (type == 0) { + return; + } + + if (type < ContextType.CONTENT.getValue() + || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index); + } + + final int subType = contextSubType != null ? contextSubType : 0; + if (subType < 0) { + throw new ValidationException( + "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); + } + + if (subType == 0) { + return; + } + + if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) { + if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) { + if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) { + if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " + + "combination. See " + documentationOnPage(39), index, context, contextSubType); + } + return; + } + + if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { + throw new ValidationException( + "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); + } + } + + private void validateNativePlacementType(Integer placementType, int index) throws ValidationException { + final int type = placementType != null ? placementType : 0; + if (type == 0) { + return; + } + + if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue() + && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type); + } + } + + private List validateAndGetUpdatedNativeAssets(List assets, int impIndex) throws ValidationException { + if (CollectionUtils.isEmpty(assets)) { + throw new ValidationException( + "request.imp[%d].native.request.assets must be an array containing at least one object", impIndex); + } + + final List updatedAssets = new ArrayList<>(); + for (int i = 0; i < assets.size(); i++) { + final Asset asset = assets.get(i); + validateNativeAsset(asset, impIndex, i); + + final Asset updatedAsset = asset.getId() != null ? asset : asset.toBuilder().id(i).build(); + final boolean hasAssetWithId = updatedAssets.stream() + .map(Asset::getId) + .anyMatch(id -> id.equals(updatedAsset.getId())); + + if (hasAssetWithId) { + throw new ValidationException("request.imp[%d].native.request.assets[%d].id is already being used by " + + "another asset. Each asset ID must be unique.", impIndex, i); + } + + updatedAssets.add(updatedAsset); + } + return updatedAssets; + } + + private void validateNativeAsset(Asset asset, int impIndex, int assetIndex) throws ValidationException { + final TitleObject title = asset.getTitle(); + final ImageObject image = asset.getImg(); + final VideoObject video = asset.getVideo(); + final DataObject data = asset.getData(); + + final long assetsCount = Stream.of(title, image, video, data) + .filter(Objects::nonNull) + .count(); + + if (assetsCount > 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", + impIndex, assetIndex); + } + + validateNativeAssetTitle(title, impIndex, assetIndex); + validateNativeAssetVideo(video, impIndex, assetIndex); + validateNativeAssetData(data, impIndex, assetIndex); + } + + private void validateNativeAssetTitle(TitleObject title, int impIndex, int assetIndex) throws ValidationException { + if (title != null && (title.getLen() == null || title.getLen() < 1)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].title.len must be a positive integer", + impIndex, assetIndex); + } + } + + private void validateNativeAssetData(DataObject data, int impIndex, int assetIndex) throws ValidationException { + if (data == null || data.getType() == null) { + return; + } + + final Integer type = data.getType(); + if (type < DataAssetType.SPONSORED.getValue() + || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: " + + documentationOnPage(40), impIndex, assetIndex); + } + } + + private void validateNativeAssetVideo(VideoObject video, int impIndex, int assetIndex) throws ValidationException { + if (video == null) { + return; + } + + if (CollectionUtils.isEmpty(video.getMimes())) { + throw new ValidationException("request.imp[%d].native.request.assets[%d].video.mimes must be an " + + "array with at least one MIME type", impIndex, assetIndex); + } + + if (video.getMinduration() == null || video.getMinduration() < 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", + impIndex, assetIndex); + } + + if (video.getMaxduration() == null || video.getMaxduration() < 1) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", + impIndex, assetIndex); + } + + validateNativeVideoProtocols(video.getProtocols(), impIndex, assetIndex); + } + + private void validateNativeVideoProtocols(List protocols, int impIndex, int assetIndex) + throws ValidationException { + if (CollectionUtils.isEmpty(protocols)) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least" + + " one element", impIndex, assetIndex); + } + + for (int i = 0; i < protocols.size(); i++) { + validateNativeVideoProtocol(protocols.get(i), impIndex, assetIndex, i); + } + } + + private void validateNativeVideoProtocol(Integer protocol, int impIndex, int assetIndex, int protocolIndex) + throws ValidationException { + if (protocol < Protocol.VAST10.getValue() || protocol > Protocol.DAAST10_WRAPPER.getValue()) { + throw new ValidationException( + "request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]." + + " Got %d", impIndex, assetIndex, protocolIndex, protocol); + } + } + + private void validateNativeEventTrackers(List eventTrackers, int impIndex) + throws ValidationException { + + if (CollectionUtils.isNotEmpty(eventTrackers)) { + for (int eventTrackerIndex = 0; eventTrackerIndex < eventTrackers.size(); eventTrackerIndex++) { + validateNativeEventTracker(eventTrackers.get(eventTrackerIndex), impIndex, eventTrackerIndex); + } + } + } + + private void validateNativeEventTracker(EventTracker eventTracker, int impIndex, int eventIndex) + throws ValidationException { + if (eventTracker != null) { + final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0; + + if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue() + && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: " + + documentationOnPage(43), impIndex, eventIndex + ); + } + + final List methods = eventTracker.getMethods(); + + if (CollectionUtils.isEmpty(methods)) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: " + + documentationOnPage(43), impIndex, eventIndex + ); + } + + for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) { + final int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0; + if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue() + && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { + throw new ValidationException( + "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: " + + documentationOnPage(43), impIndex, eventIndex, methodIndex + ); + } + } + } + } + + private void validateImpExt(ObjectNode ext, Map aliases, int impIndex, + List warnings) throws ValidationException { + validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex, warnings); + } + + private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex, + List warnings) + throws ValidationException { + + if (extPrebidNode == null) { + throw new ValidationException( + "request.imp[%d].ext.prebid must be defined", impIndex); + } + + if (!extPrebidNode.isObject()) { + throw new ValidationException( + "request.imp[%d].ext.prebid must an object type", impIndex); + } + + final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT); + + if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder must be an object type", impIndex); + } + final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex); + + validateImpExtPrebidBidder(extPrebidBidderNode, extPrebid.getStoredAuctionResponse(), + aliases, impIndex, warnings); + validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex, warnings); + + validateImpExtPrebidImp(extPrebidNode.get(IMP_EXT), aliases, impIndex, warnings); + } + + private void validateImpExtPrebidImp(JsonNode imp, + Map aliases, + int impIndex, + List warnings) { + if (imp == null) { + return; + } + + final Iterator> bidders = imp.fields(); + while (bidders.hasNext()) { + final Map.Entry bidder = bidders.next(); + final String bidderName = bidder.getKey(); + final String resolvedBidderName = aliases.getOrDefault(bidderName, bidderName); + if (!bidderCatalog.isValidName(resolvedBidderName) && !bidderCatalog.isDeprecatedName(resolvedBidderName)) { + bidders.remove(); + warnings.add("WARNING: request.imp[%d].ext.prebid.imp.%s was dropped with the reason: invalid bidder" + .formatted(impIndex, bidderName)); + } + } + } + + private void validateImpExtPrebidBidder(JsonNode extPrebidBidder, + ExtStoredAuctionResponse storedAuctionResponse, + Map aliases, + int impIndex, + List warnings) throws ValidationException { + if (extPrebidBidder == null) { + if (storedAuctionResponse != null) { + return; + } else { + throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex); + } + } + + final Iterator> bidderExtensions = extPrebidBidder.fields(); + while (bidderExtensions.hasNext()) { + final Map.Entry bidderExtension = bidderExtensions.next(); + final String bidder = bidderExtension.getKey(); + try { + validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); + } catch (ValidationException ex) { + bidderExtensions.remove(); + warnings.add("WARNING: request.imp[%d].ext.prebid.bidder.%s was dropped with a reason: %s" + .formatted(impIndex, bidder, ex.getMessage())); + } + } + + if (extPrebidBidder.isEmpty()) { + warnings.add("WARNING: request.imp[%d].ext must contain at least one valid bidder".formatted(impIndex)); + } + } + + private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, + Map aliases, + int impIndex, + List warnings) throws ValidationException { + final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse(); + if (extStoredAuctionResponse != null) { + if (extStoredAuctionResponse.getSeatBids() != null) { + warnings.add("WARNING: request.imp[%d].ext.prebid.storedauctionresponse.seatbidarr".formatted(impIndex) + + " is not supported at the imp level"); + } + + if (extStoredAuctionResponse.getId() == null) { + throw new ValidationException("request.imp[%d].ext.prebid.storedauctionresponse.id should be defined", + impIndex); + } + } + + final List storedBidResponses = extPrebid.getStoredBidResponse(); + if (CollectionUtils.isNotEmpty(storedBidResponses)) { + final ObjectNode bidderNode = extPrebid.getBidder(); + if (bidderNode == null || bidderNode.isEmpty()) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse" + .formatted(impIndex)); + } + + for (ExtStoredBidResponse storedBidResponse : storedBidResponses) { + validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex); + } + } + } + + private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode, + Map aliases, int impIndex) throws ValidationException { + final String bidder = extStoredBidResponse.getBidder(); + final String id = extStoredBidResponse.getId(); + if (StringUtils.isEmpty(bidder)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined".formatted(impIndex)); + } + + if (StringUtils.isEmpty(id)) { + throw new ValidationException( + "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id".formatted(impIndex)); + } + + final String resolvedBidder = aliases.getOrDefault(bidder, bidder); + + if (!bidderCatalog.isValidName(resolvedBidder)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder".formatted(impIndex)); + } + + final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames()) + .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder)); + if (noCorrespondentBidderParameters) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters" + .formatted(impIndex)); + } + } + + private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException { + try { + return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new ValidationException(" bidRequest.imp[%d].ext.prebid: %s has invalid format" + .formatted(impIndex, e.getMessage())); + } + } + + private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName) + throws ValidationException { + if (bidderCatalog.isValidName(bidderName)) { + final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); + if (!messages.isEmpty()) { + throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, + bidderName, String.join("\n", messages)); + } + } else if (!bidderCatalog.isDeprecatedName(bidderName)) { + throw new ValidationException( + "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); + } + } + + private void validatePmp(Pmp pmp, String msgPrefix) throws ValidationException { + if (pmp != null && pmp.getDeals() != null) { + for (int dealIndex = 0; dealIndex < pmp.getDeals().size(); dealIndex++) { + if (StringUtils.isBlank(pmp.getDeals().get(dealIndex).getId())) { + throw new ValidationException("%s.pmp.deals[%d] missing required field: \"id\"", + msgPrefix, dealIndex); + } + } + } + } + + private void validateBanner(Banner banner, boolean isInterstitial, String msgPrefix) throws ValidationException { + if (banner != null) { + final Integer width = banner.getW(); + final Integer height = banner.getH(); + final boolean hasWidth = hasPositiveValue(width); + final boolean hasHeight = hasPositiveValue(height); + final boolean hasSize = hasWidth && hasHeight; + + final List format = banner.getFormat(); + if (CollectionUtils.isEmpty(format) && !hasSize && !isInterstitial) { + throw new ValidationException("%s.banner has no sizes. Define \"w\" and \"h\", " + + "or include \"format\" elements", msgPrefix); + } + + if (width != null && height != null && !hasSize && !isInterstitial) { + throw new ValidationException("%s.banner must define a valid" + + " \"h\" and \"w\" properties", msgPrefix); + } + + if (format != null) { + for (int formatIndex = 0; formatIndex < format.size(); formatIndex++) { + validateFormat(format.get(formatIndex), msgPrefix, formatIndex); + } + } + } + } + + private void validateFormat(Format format, String msgPrefix, int formatIndex) throws ValidationException { + final boolean usesH = hasPositiveValue(format.getH()); + final boolean usesW = hasPositiveValue(format.getW()); + final boolean usesWmin = hasPositiveValue(format.getWmin()); + final boolean usesWratio = hasPositiveValue(format.getWratio()); + final boolean usesHratio = hasPositiveValue(format.getHratio()); + final boolean usesHW = usesH || usesW; + final boolean usesRatios = usesWmin || usesWratio || usesHratio; + + if (usesHW && usesRatios) { + throw new ValidationException("%s.banner.format[%d] should define *either*" + + " {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" " + + "objects in the request", msgPrefix, formatIndex); + } + + if (!usesHW && !usesRatios) { + throw new ValidationException("%s.banner.format[%d] should define *either*" + + " {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) " + + "to be non-zero positive", msgPrefix, formatIndex); + } + + if (usesHW && (!usesH || !usesW)) { + throw new ValidationException("%s.banner.format[%d] must define a valid" + + " \"h\" and \"w\" properties", msgPrefix, formatIndex); + } + + if (usesRatios && (!usesWmin || !usesWratio || !usesHratio)) { + throw new ValidationException("%s.banner.format[%d] must define a valid" + + " \"wmin\", \"wratio\", and \"hratio\" properties", msgPrefix, formatIndex); + } + } + + private void validateVideoMimes(Video video, String msgPrefix) throws ValidationException { + if (video != null) { + validateMimes(video.getMimes(), + "%s.video.mimes must contain at least one supported MIME type", msgPrefix); + } + } + + private void validateAudioMimes(Audio audio, String msgPrefix) throws ValidationException { + if (audio != null) { + validateMimes(audio.getMimes(), + "%s.audio.mimes must contain at least one supported MIME type", msgPrefix); + } + } + + private void validateMimes(List mimes, String msg, String msgPrefix) throws ValidationException { + if (CollectionUtils.isEmpty(mimes)) { + throw new ValidationException(msg, msgPrefix); + } + } + + private void validateMetrics(List metrics, String msgPrefix) throws ValidationException { + for (int i = 0; i < metrics.size(); i++) { + final Metric metric = metrics.get(i); + + if (StringUtils.isEmpty(metric.getType())) { + throw new ValidationException("Missing %s.metric[%d].type", msgPrefix, i); + } + + final Float value = metric.getValue(); + if (value == null || value < 0.0 || value > 1.0) { + throw new ValidationException("%s.metric[%d].value must be in the range [0.0, 1.0]", msgPrefix, i); + } + } + } + + private String toEncodedRequest(Request nativeRequest, List updatedAssets) { + try { + return mapper.mapper().writeValueAsString(nativeRequest.toBuilder().assets(updatedAssets).build()); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Error while marshaling native request to the string", e); + } + } + + private static String documentationOnPage(int page) { + return "%s#page=%d".formatted(DOCUMENTATION, page); + } + + private static boolean hasPositiveValue(Integer value) { + return value != null && value > 0; + } + +} diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index 6076aefebe9..bece88c27ac 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -3,41 +3,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Asset; -import com.iab.openrtb.request.Audio; -import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.DataObject; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.EventTracker; -import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.ImageObject; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Metric; -import com.iab.openrtb.request.Native; -import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Request; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.TitleObject; import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; -import com.iab.openrtb.request.VideoObject; -import com.iab.openrtb.request.ntv.ContextSubType; -import com.iab.openrtb.request.ntv.ContextType; -import com.iab.openrtb.request.ntv.DataAssetType; -import com.iab.openrtb.request.ntv.EventTrackingMethod; -import com.iab.openrtb.request.ntv.EventType; -import com.iab.openrtb.request.ntv.PlacementType; -import com.iab.openrtb.request.ntv.Protocol; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; @@ -49,7 +27,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -60,30 +37,24 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.StreamUtil; import org.prebid.server.validation.model.ValidationResult; -import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; /** * A component that validates {@link BidRequest} objects for openrtb2 auction endpoint. @@ -94,17 +65,11 @@ public class RequestValidator { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( LoggerFactory.getLogger(RequestValidator.class)); - private static final String PREBID_EXT = "prebid"; - private static final String BIDDER_EXT = "bidder"; private static final String ASTERISK = "*"; private static final Locale LOCALE = Locale.US; - private static final Integer NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND = 500; - - private static final String DOCUMENTATION = "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf"; private final BidderCatalog bidderCatalog; - private final BidderParamValidator bidderParamValidator; + private final ImpValidator impValidator; private final Metrics metrics; private final JacksonMapper mapper; private final double logSamplingRate; @@ -115,14 +80,13 @@ public class RequestValidator { * properties of bidRequest. */ public RequestValidator(BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, - Metrics metrics, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, double logSamplingRate, boolean enabledStrictAppSiteDoohValidation) { this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.bidderParamValidator = Objects.requireNonNull(bidderParamValidator); + this.impValidator = Objects.requireNonNull(impValidator); this.metrics = Objects.requireNonNull(metrics); this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; @@ -186,9 +150,7 @@ public ValidationResult validate(BidRequest bidRequest, HttpRequestContext httpR throw new ValidationException(String.join(System.lineSeparator(), errors)); } - for (int index = 0; index < bidRequest.getImp().size(); index++) { - validateImp(bidRequest.getImp().get(index), aliases, index, warnings); - } + impValidator.validateImps(bidRequest.getImp(), aliases, warnings); final List channels = new ArrayList<>(); Optional.ofNullable(bidRequest.getApp()).ifPresent(ignored -> channels.add("request.app")); @@ -660,557 +622,4 @@ private void validateRegs(Regs regs) throws ValidationException { } } - private void validateImp(Imp imp, Map aliases, int index, List warnings) - throws ValidationException { - if (StringUtils.isBlank(imp.getId())) { - throw new ValidationException("request.imp[%d] missing required field: \"id\"", index); - } - if (imp.getMetric() != null && !imp.getMetric().isEmpty()) { - validateMetrics(imp.getMetric(), index); - } - if (imp.getBanner() == null && imp.getVideo() == null && imp.getAudio() == null && imp.getXNative() == null) { - throw new ValidationException( - "request.imp[%d] must contain at least one of \"banner\", \"video\", \"audio\", or \"native\"", - index); - } - - final boolean isInterstitialImp = Objects.equals(imp.getInstl(), 1); - validateBanner(imp.getBanner(), isInterstitialImp, index); - validateVideoMimes(imp.getVideo(), index); - validateAudioMimes(imp.getAudio(), index); - fillAndValidateNative(imp.getXNative(), index); - validatePmp(imp.getPmp(), index); - validateImpExt(imp.getExt(), aliases, index, warnings); - } - - private void fillAndValidateNative(Native xNative, int impIndex) throws ValidationException { - if (xNative == null) { - return; - } - - final Request nativeRequest = parseNativeRequest(xNative.getRequest(), impIndex); - - validateNativeContextTypes(nativeRequest.getContext(), nativeRequest.getContextsubtype(), impIndex); - validateNativePlacementType(nativeRequest.getPlcmttype(), impIndex); - final List updatedAssets = validateAndGetUpdatedNativeAssets(nativeRequest.getAssets(), impIndex); - validateNativeEventTrackers(nativeRequest.getEventtrackers(), impIndex); - - // modifier was added to reduce memory consumption on updating bidRequest.imp[i].native.request object - xNative.setRequest(toEncodedRequest(nativeRequest, updatedAssets)); - } - - private Request parseNativeRequest(String rawStringNativeRequest, int impIndex) throws ValidationException { - if (StringUtils.isBlank(rawStringNativeRequest)) { - throw new ValidationException("request.imp[%d].native contains empty request value", impIndex); - } - try { - return mapper.mapper().readValue(rawStringNativeRequest, Request.class); - } catch (IOException e) { - throw new ValidationException("Error while parsing request.imp[%d].native.request: %s", - impIndex, - ExceptionUtils.getMessage(e)); - } - } - - private void validateNativeContextTypes(Integer context, Integer contextSubType, int index) - throws ValidationException { - - final int type = context != null ? context : 0; - if (type == 0) { - return; - } - - if (type < ContextType.CONTENT.getValue() - || (type > ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.context is invalid. See " + documentationOnPage(39), index); - } - - final int subType = contextSubType != null ? contextSubType : 0; - if (subType < 0) { - throw new ValidationException( - "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); - } - - if (subType == 0) { - return; - } - - if (subType >= ContextSubType.GENERAL.getValue() && subType <= ContextSubType.USER_GENERATED.getValue()) { - if (type != ContextType.CONTENT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType >= ContextSubType.SOCIAL.getValue() && subType <= ContextSubType.CHAT.getValue()) { - if (type != ContextType.SOCIAL.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType >= ContextSubType.SELLING.getValue() && subType <= ContextSubType.PRODUCT_REVIEW.getValue()) { - if (type != ContextType.PRODUCT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.context is %d, but contextsubtype is %d. This is an invalid " - + "combination. See " + documentationOnPage(39), index, context, contextSubType); - } - return; - } - - if (subType < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND) { - throw new ValidationException( - "request.imp[%d].native.request.contextsubtype is invalid. See " + documentationOnPage(39), index); - } - } - - private void validateNativePlacementType(Integer placementType, int index) throws ValidationException { - final int type = placementType != null ? placementType : 0; - if (type == 0) { - return; - } - - if (type < PlacementType.FEED.getValue() || (type > PlacementType.RECOMMENDATION_WIDGET.getValue() - && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.plcmttype is invalid. See " + documentationOnPage(40), index, type); - } - } - - private List validateAndGetUpdatedNativeAssets(List assets, int impIndex) throws ValidationException { - if (CollectionUtils.isEmpty(assets)) { - throw new ValidationException( - "request.imp[%d].native.request.assets must be an array containing at least one object", impIndex); - } - - final List updatedAssets = new ArrayList<>(); - for (int i = 0; i < assets.size(); i++) { - final Asset asset = assets.get(i); - validateNativeAsset(asset, impIndex, i); - - final Asset updatedAsset = asset.getId() != null ? asset : asset.toBuilder().id(i).build(); - final boolean hasAssetWithId = updatedAssets.stream() - .map(Asset::getId) - .anyMatch(id -> id.equals(updatedAsset.getId())); - - if (hasAssetWithId) { - throw new ValidationException("request.imp[%d].native.request.assets[%d].id is already being used by " - + "another asset. Each asset ID must be unique.", impIndex, i); - } - - updatedAssets.add(updatedAsset); - } - return updatedAssets; - } - - private void validateNativeAsset(Asset asset, int impIndex, int assetIndex) throws ValidationException { - final TitleObject title = asset.getTitle(); - final ImageObject image = asset.getImg(); - final VideoObject video = asset.getVideo(); - final DataObject data = asset.getData(); - - final long assetsCount = Stream.of(title, image, video, data) - .filter(Objects::nonNull) - .count(); - - if (assetsCount > 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d] must define at most one of {title, img, video, data}", - impIndex, assetIndex); - } - - validateNativeAssetTitle(title, impIndex, assetIndex); - validateNativeAssetVideo(video, impIndex, assetIndex); - validateNativeAssetData(data, impIndex, assetIndex); - } - - private void validateNativeAssetTitle(TitleObject title, int impIndex, int assetIndex) throws ValidationException { - if (title != null && (title.getLen() == null || title.getLen() < 1)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].title.len must be a positive integer", - impIndex, assetIndex); - } - } - - private void validateNativeAssetData(DataObject data, int impIndex, int assetIndex) throws ValidationException { - if (data == null || data.getType() == null) { - return; - } - - final Integer type = data.getType(); - if (type < DataAssetType.SPONSORED.getValue() - || (type > DataAssetType.CTA_TEXT.getValue() && type < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].data.type is invalid. See section 7.4: " - + documentationOnPage(40), impIndex, assetIndex); - } - } - - private void validateNativeAssetVideo(VideoObject video, int impIndex, int assetIndex) throws ValidationException { - if (video == null) { - return; - } - - if (CollectionUtils.isEmpty(video.getMimes())) { - throw new ValidationException("request.imp[%d].native.request.assets[%d].video.mimes must be an " - + "array with at least one MIME type", impIndex, assetIndex); - } - - if (video.getMinduration() == null || video.getMinduration() < 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.minduration must be a positive integer", - impIndex, assetIndex); - } - - if (video.getMaxduration() == null || video.getMaxduration() < 1) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.maxduration must be a positive integer", - impIndex, assetIndex); - } - - validateNativeVideoProtocols(video.getProtocols(), impIndex, assetIndex); - } - - private void validateNativeVideoProtocols(List protocols, int impIndex, int assetIndex) - throws ValidationException { - if (CollectionUtils.isEmpty(protocols)) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.protocols must be an array with at least" - + " one element", impIndex, assetIndex); - } - - for (int i = 0; i < protocols.size(); i++) { - validateNativeVideoProtocol(protocols.get(i), impIndex, assetIndex, i); - } - } - - private void validateNativeVideoProtocol(Integer protocol, int impIndex, int assetIndex, int protocolIndex) - throws ValidationException { - if (protocol < Protocol.VAST10.getValue() || protocol > Protocol.DAAST10_WRAPPER.getValue()) { - throw new ValidationException( - "request.imp[%d].native.request.assets[%d].video.protocols[%d] must be in the range [1, 10]." - + " Got %d", impIndex, assetIndex, protocolIndex, protocol); - } - } - - private void validateNativeEventTrackers(List eventTrackers, int impIndex) - throws ValidationException { - - if (CollectionUtils.isNotEmpty(eventTrackers)) { - for (int eventTrackerIndex = 0; eventTrackerIndex < eventTrackers.size(); eventTrackerIndex++) { - validateNativeEventTracker(eventTrackers.get(eventTrackerIndex), impIndex, eventTrackerIndex); - } - } - } - - private void validateNativeEventTracker(EventTracker eventTracker, int impIndex, int eventIndex) - throws ValidationException { - if (eventTracker != null) { - final int event = eventTracker.getEvent() != null ? eventTracker.getEvent() : 0; - - if (event != 0 && (event < EventType.IMPRESSION.getValue() || (event > EventType.VIEWABLE_VIDEO50.getValue() - && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND))) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].event is invalid. See section 7.6: " - + documentationOnPage(43), impIndex, eventIndex - ); - } - - final List methods = eventTracker.getMethods(); - - if (CollectionUtils.isEmpty(methods)) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].method is required. See section 7.7: " - + documentationOnPage(43), impIndex, eventIndex - ); - } - - for (int methodIndex = 0; methodIndex < methods.size(); methodIndex++) { - final int method = methods.get(methodIndex) != null ? methods.get(methodIndex) : 0; - if (method < EventTrackingMethod.IMAGE.getValue() || (method > EventTrackingMethod.JS.getValue() - && event < NATIVE_EXCHANGE_SPECIFIC_LOWER_BOUND)) { - throw new ValidationException( - "request.imp[%d].native.request.eventtrackers[%d].methods[%d] is invalid. See section 7.7: " - + documentationOnPage(43), impIndex, eventIndex, methodIndex - ); - } - } - } - } - - private static String documentationOnPage(int page) { - return "%s#page=%d".formatted(DOCUMENTATION, page); - } - - private String toEncodedRequest(Request nativeRequest, List updatedAssets) { - try { - return mapper.mapper().writeValueAsString(nativeRequest.toBuilder().assets(updatedAssets).build()); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Error while marshaling native request to the string", e); - } - } - - private void validateImpExt(ObjectNode ext, Map aliases, int impIndex, - List warnings) throws ValidationException { - validateImpExtPrebid(ext != null ? ext.get(PREBID_EXT) : null, aliases, impIndex, warnings); - } - - private void validateImpExtPrebid(JsonNode extPrebidNode, Map aliases, int impIndex, - List warnings) - throws ValidationException { - - if (extPrebidNode == null) { - throw new ValidationException( - "request.imp[%d].ext.prebid must be defined", impIndex); - } - - if (!extPrebidNode.isObject()) { - throw new ValidationException( - "request.imp[%d].ext.prebid must an object type", impIndex); - } - - final JsonNode extPrebidBidderNode = extPrebidNode.get(BIDDER_EXT); - - if (extPrebidBidderNode != null && !extPrebidBidderNode.isObject()) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder must be an object type", impIndex); - } - final ExtImpPrebid extPrebid = parseExtImpPrebid((ObjectNode) extPrebidNode, impIndex); - - validateImpExtPrebidBidder(extPrebidBidderNode, extPrebid.getStoredAuctionResponse(), - aliases, impIndex, warnings); - validateImpExtPrebidStoredResponses(extPrebid, aliases, impIndex, warnings); - } - - private void validateImpExtPrebidBidder(JsonNode extPrebidBidder, - ExtStoredAuctionResponse storedAuctionResponse, - Map aliases, - int impIndex, - List warnings) throws ValidationException { - if (extPrebidBidder == null) { - if (storedAuctionResponse != null) { - return; - } else { - throw new ValidationException("request.imp[%d].ext.prebid.bidder must be defined", impIndex); - } - } - - final Iterator> bidderExtensions = extPrebidBidder.fields(); - while (bidderExtensions.hasNext()) { - final Map.Entry bidderExtension = bidderExtensions.next(); - final String bidder = bidderExtension.getKey(); - try { - validateImpBidderExtName(impIndex, bidderExtension, aliases.getOrDefault(bidder, bidder)); - } catch (ValidationException ex) { - bidderExtensions.remove(); - warnings.add("WARNING: request.imp[%d].ext.prebid.bidder.%s was dropped with a reason: %s" - .formatted(impIndex, bidder, ex.getMessage())); - } - } - - if (extPrebidBidder.isEmpty()) { - warnings.add("WARNING: request.imp[%d].ext must contain at least one valid bidder".formatted(impIndex)); - } - } - - private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, - Map aliases, - int impIndex, - List warnings) throws ValidationException { - - final ExtStoredAuctionResponse extStoredAuctionResponse = extPrebid.getStoredAuctionResponse(); - if (extStoredAuctionResponse != null) { - if (extStoredAuctionResponse.getSeatBids() != null) { - warnings.add("WARNING: request.imp[%d].ext.prebid.storedauctionresponse.seatbidarr".formatted(impIndex) - + " is not supported at the imp level"); - } - - if (extStoredAuctionResponse.getId() == null) { - throw new ValidationException("request.imp[%d].ext.prebid.storedauctionresponse.id should be defined", - impIndex); - } - } - - final List storedBidResponses = extPrebid.getStoredBidResponse(); - if (CollectionUtils.isNotEmpty(storedBidResponses)) { - final ObjectNode bidderNode = extPrebid.getBidder(); - if (bidderNode == null || bidderNode.isEmpty()) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder should be defined for storedbidresponse" - .formatted(impIndex)); - } - - for (ExtStoredBidResponse storedBidResponse : storedBidResponses) { - validateStoredBidResponse(storedBidResponse, bidderNode, aliases, impIndex); - } - } - } - - private void validateStoredBidResponse(ExtStoredBidResponse extStoredBidResponse, ObjectNode bidderNode, - Map aliases, int impIndex) throws ValidationException { - final String bidder = extStoredBidResponse.getBidder(); - final String id = extStoredBidResponse.getId(); - if (StringUtils.isEmpty(bidder)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder was not defined".formatted(impIndex)); - } - - if (StringUtils.isEmpty(id)) { - throw new ValidationException( - "Id was not defined for request.imp[%d].ext.prebid.storedbidresponse.id".formatted(impIndex)); - } - - final String resolvedBidder = aliases.getOrDefault(bidder, bidder); - - if (!bidderCatalog.isValidName(resolvedBidder)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder is not valid bidder".formatted(impIndex)); - } - - final boolean noCorrespondentBidderParameters = StreamUtil.asStream(bidderNode.fieldNames()) - .noneMatch(impBidder -> impBidder.equals(resolvedBidder) || impBidder.equals(bidder)); - if (noCorrespondentBidderParameters) { - throw new ValidationException( - "request.imp[%d].ext.prebid.storedbidresponse.bidder does not have correspondent bidder parameters" - .formatted(impIndex)); - } - } - - private ExtImpPrebid parseExtImpPrebid(ObjectNode extImpPrebid, int impIndex) throws ValidationException { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new ValidationException(" bidRequest.imp[%d].ext.prebid: %s has invalid format" - .formatted(impIndex, e.getMessage())); - } - } - - private void validateImpBidderExtName(int impIndex, Map.Entry bidderExtension, String bidderName) - throws ValidationException { - if (bidderCatalog.isValidName(bidderName)) { - final Set messages = bidderParamValidator.validate(bidderName, bidderExtension.getValue()); - if (!messages.isEmpty()) { - throw new ValidationException("request.imp[%d].ext.prebid.bidder.%s failed validation.\n%s", impIndex, - bidderName, String.join("\n", messages)); - } - } else if (!bidderCatalog.isDeprecatedName(bidderName)) { - throw new ValidationException( - "request.imp[%d].ext.prebid.bidder contains unknown bidder: %s", impIndex, bidderName); - } - } - - private void validatePmp(Pmp pmp, int impIndex) throws ValidationException { - if (pmp != null && pmp.getDeals() != null) { - for (int dealIndex = 0; dealIndex < pmp.getDeals().size(); dealIndex++) { - if (StringUtils.isBlank(pmp.getDeals().get(dealIndex).getId())) { - throw new ValidationException("request.imp[%d].pmp.deals[%d] missing required field: \"id\"", - impIndex, dealIndex); - } - } - } - } - - private void validateBanner(Banner banner, boolean isInterstitial, int impIndex) throws ValidationException { - if (banner != null) { - final Integer width = banner.getW(); - final Integer height = banner.getH(); - final boolean hasWidth = hasPositiveValue(width); - final boolean hasHeight = hasPositiveValue(height); - final boolean hasSize = hasWidth && hasHeight; - - final List format = banner.getFormat(); - if (CollectionUtils.isEmpty(format) && !hasSize && !isInterstitial) { - throw new ValidationException("request.imp[%d].banner has no sizes. Define \"w\" and \"h\", " - + "or include \"format\" elements", impIndex); - } - - if (width != null && height != null && !hasSize && !isInterstitial) { - throw new ValidationException("Request imp[%d].banner must define a valid" - + " \"h\" and \"w\" properties", impIndex); - } - - if (format != null) { - for (int formatIndex = 0; formatIndex < format.size(); formatIndex++) { - validateFormat(format.get(formatIndex), impIndex, formatIndex); - } - } - } - } - - private void validateFormat(Format format, int impIndex, int formatIndex) throws ValidationException { - final boolean usesH = hasPositiveValue(format.getH()); - final boolean usesW = hasPositiveValue(format.getW()); - final boolean usesWmin = hasPositiveValue(format.getWmin()); - final boolean usesWratio = hasPositiveValue(format.getWratio()); - final boolean usesHratio = hasPositiveValue(format.getHratio()); - final boolean usesHW = usesH || usesW; - final boolean usesRatios = usesWmin || usesWratio || usesHratio; - - if (usesHW && usesRatios) { - throw new ValidationException("Request imp[%d].banner.format[%d] should define *either*" - + " {w, h} *or* {wmin, wratio, hratio}, but not both. If both are valid, send two \"format\" " - + "objects in the request", impIndex, formatIndex); - } - - if (!usesHW && !usesRatios) { - throw new ValidationException("Request imp[%d].banner.format[%d] should define *either*" - + " {w, h} (for static size requirements) *or* {wmin, wratio, hratio} (for flexible sizes) " - + "to be non-zero positive", impIndex, formatIndex); - } - - if (usesHW && (!usesH || !usesW)) { - throw new ValidationException("Request imp[%d].banner.format[%d] must define a valid" - + " \"h\" and \"w\" properties", impIndex, formatIndex); - } - - if (usesRatios && (!usesWmin || !usesWratio || !usesHratio)) { - throw new ValidationException("Request imp[%d].banner.format[%d] must define a valid" - + " \"wmin\", \"wratio\", and \"hratio\" properties", impIndex, formatIndex); - } - } - - private void validateVideoMimes(Video video, int impIndex) throws ValidationException { - if (video != null) { - validateMimes(video.getMimes(), - "request.imp[%d].video.mimes must contain at least one supported MIME type", impIndex); - } - } - - private void validateAudioMimes(Audio audio, int impIndex) throws ValidationException { - if (audio != null) { - validateMimes(audio.getMimes(), - "request.imp[%d].audio.mimes must contain at least one supported MIME type", impIndex); - } - } - - private void validateMimes(List mimes, String msg, int index) throws ValidationException { - if (CollectionUtils.isEmpty(mimes)) { - throw new ValidationException(msg, index); - } - } - - private void validateMetrics(List metrics, int impIndex) throws ValidationException { - for (int i = 0; i < metrics.size(); i++) { - final Metric metric = metrics.get(i); - - if (StringUtils.isEmpty(metric.getType())) { - throw new ValidationException("Missing request.imp[%d].metric[%d].type", impIndex, i); - } - - final Float value = metric.getValue(); - if (value == null || value < 0.0 || value > 1.0) { - throw new ValidationException("request.imp[%d].metric[%d].value must be in the range [0.0, 1.0]", - impIndex, i); - } - } - } - - private static boolean hasPositiveValue(Integer value) { - return value != null && value > 0; - } } diff --git a/src/main/java/org/prebid/server/validation/ValidationException.java b/src/main/java/org/prebid/server/validation/ValidationException.java index 792d8a047bc..c5f4efae562 100644 --- a/src/main/java/org/prebid/server/validation/ValidationException.java +++ b/src/main/java/org/prebid/server/validation/ValidationException.java @@ -1,12 +1,12 @@ package org.prebid.server.validation; -class ValidationException extends Exception { +public class ValidationException extends Exception { - ValidationException(String errorMessageFormat) { + public ValidationException(String errorMessageFormat) { super(errorMessageFormat); } - ValidationException(String errorMessageFormat, Object... args) { + public ValidationException(String errorMessageFormat, Object... args) { super(errorMessageFormat.formatted(args)); } } diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy index afdd81c19ef..1502df5ed70 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy @@ -10,6 +10,7 @@ enum BidderName { EMPTY(""), BOGUS("bogus"), ALIAS("alias"), + ALIAS_CAMEL_CASE("AlIaS"), GENERIC_CAMEL_CASE("GeNerIc"), GENERIC("generic"), RUBICON("rubicon"), diff --git a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy index 58017ce1490..c1889dc51bc 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImp.groovy @@ -4,8 +4,8 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.Imp -@ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) class BidderImp extends Imp { BidderImpExt ext diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy index 9895f860b40..6dbe31760f4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Deal.groovy @@ -2,8 +2,11 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class Deal { @@ -15,4 +18,8 @@ class Deal { List wseat List wadomain DealExt ext -} + + static Deal getDefaultDeal() { + new Deal(id: PBSUtils.randomString) + } + } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy index 90b57aa8fb9..d59c145551a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealExt.groovy @@ -1,7 +1,9 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true) class DealExt { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy index 4ab7823193c..42b54c6522a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DealLineItem.groovy @@ -2,8 +2,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class DealLineItem { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy index 3508dfa60fe..f6d1798ca57 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Format.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class Format { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy index 21782323974..fc771e59919 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExtPrebid.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @@ -17,6 +18,7 @@ class ImpExtPrebid { Bidder bidder ImpExtPrebidFloors floors Map passThrough + Map imp static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy index ce72554490b..15fb3a6d464 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Pmp.groovy @@ -2,12 +2,18 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class Pmp { Integer privateAuction List deals + + static Pmp getDefaultPmp() { + new Pmp(deals: [Deal.defaultDeal]) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy index b6cfe6d0313..bd7d6abfafc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderFormatSpec.groovy @@ -68,7 +68,7 @@ class BidderFormatSpec extends BaseSpec { def exception = thrown(PrebidServerException) assert exception.statusCode == 400 assert exception.responseBody == "Invalid request format: " + - "Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties" + "request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties" where: bannerFormatWeight | bannerFormatHeight @@ -92,7 +92,7 @@ class BidderFormatSpec extends BaseSpec { then: "PBs should throw error due to banner.format{w.h} validation" def exception = thrown(PrebidServerException) assert exception.statusCode == 400 - assert exception.responseBody == "Invalid request format: Request imp[0].banner.format[0] " + + assert exception.responseBody == "Invalid request format: request.imp[0].banner.format[0] " + "should define *either* {w, h} (for static size requirements) " + "*or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive" diff --git a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy new file mode 100644 index 00000000000..bf34515eb04 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy @@ -0,0 +1,229 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Pmp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.EMPTY +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID + +class ImpRequestSpec extends BaseSpec { + + private final PrebidServerService defaultPbsServiceWithAlias = pbsServiceFactory.getService(GENERIC_ALIAS_CONFIG) + private static final String EMPTY_ID = "" + + def "PBS should update imp fields when imp.ext.prebid.imp contain bidder information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = Pmp.defaultPmp + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + ext.prebid.imp = [(bidderName): new Imp(pmp: extPmp)] + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [extPmp] + + and: "BidderRequest should contain original stored request id" + assert bidderRequest.imp.ext.prebid.storedRequest.id == [storedRequestId] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert bidderRequest?.imp?.ext?.prebid?.imp == [null] + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should update only required imp when it contain bidder information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def impWithParameters = Imp.defaultImpression.tap { + pmp = Pmp.defaultPmp + ext.prebid.imp = [(bidderName): new Imp(pmp: extPmp)] + } + def impWithoutParameters = Imp.defaultImpression.tap { + pmp = Pmp.defaultPmp + } + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [impWithParameters, impWithoutParameters] + } + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value only for required imp" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.find { it.id == impWithParameters.id }?.pmp == extPmp + assert bidderRequest.imp.find { it.id == impWithoutParameters.id }?.pmp == impWithoutParameters.pmp + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [GENERIC, GENERIC_CAMEL_CASE] + } + + def "PBS should update imp fields when imp.ext.prebid.imp contain bidder alias information"() { + given: "Default basic BidRequest" + def extPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = Pmp.defaultPmp + ext.prebid.imp = [(aliasName): new Imp(pmp: extPmp)] + } + ext.prebid.aliases = [(aliasName.value): GENERIC] + } + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [extPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + aliasName | bidderName + ALIAS | GENERIC + ALIAS_CAMEL_CASE | GENERIC + ALIAS | GENERIC_CAMEL_CASE + ALIAS_CAMEL_CASE | GENERIC_CAMEL_CASE + } + + def "PBS shouldn't update imp fields when imp.ext.prebid.imp contain only bidder with invalid name"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(bidderName): new Imp(pmp: Pmp.defaultPmp)] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["WARNING: request.imp[0].ext.prebid.imp.${bidderName} was dropped with the reason: invalid bidder"] + + and: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + bidderName << [WILDCARD, UNKNOWN] + } + + def "PBS should validate imp and add proper warning when imp.ext.prebid.imp contain invalid ortb data"() { + given: "BidRequest with invalid config for ext.prebid.imp" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(GENERIC): Imp.defaultImpression.tap { + id = EMPTY_ID + }] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["imp.ext.prebid.imp.generic can not be merged into original imp [id=${bidRequest.imp.first.id}], " + + "reason: imp[id=] missing required field: \"id\""] + + and: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + } + + def "PBS shouldn't update imp fields when imp.ext.prebid.imp contain invalid empty data"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = prebidImp + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + when: "Requesting PBS auction" + defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "BidderRequest shouldn't update imp information based on imp.ext.prebid.imp value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "BidderRequest should contain original stored request id" + assert bidderRequest.imp.ext.prebid.storedRequest.id == [storedRequestId] + + and: "PBS should remove imp.ext.prebid.imp.pmp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp?.get(GENERIC)?.pmp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + + where: + prebidImp << [ + null, + [:], + [(EMPTY): new Imp(pmp: Pmp.defaultPmp)], + [(GENERIC): null], + [(GENERIC): new Imp()], + [(GENERIC): new Imp(pmp: new Pmp())] + ] + } +} diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 1f72014ea59..40dd296723b 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -230,6 +230,9 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private SupplyChainResolver supplyChainResolver; + @Mock(strictness = LENIENT) + private ImpAdjuster impAdjuster; + @Mock private DebugResolver debugResolver; @@ -349,6 +352,8 @@ public void setUp() { given(fpdResolver.resolveImpExt(any(), anyBoolean())) .willAnswer(invocation -> invocation.getArgument(0)); + given(impAdjuster.adjust(any(), any(), any(), any())).willAnswer(invocation -> invocation.getArgument(0)); + given(supplyChainResolver.resolveForBidder(anyString(), any())).willReturn(null); given(hookStageExecutor.executeBidderRequestStage(any(), any())) @@ -4713,7 +4718,7 @@ private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { storedResponseProcessor, privacyEnforcementService, fpdResolver, - supplyChainResolver, + impAdjuster, supplyChainResolver, debugResolver, mediaTypeProcessor, uidUpdater, diff --git a/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java b/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java new file mode 100644 index 00000000000..4f8e069499c --- /dev/null +++ b/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java @@ -0,0 +1,278 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Pmp; +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.BidderCatalog; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.validation.ImpValidator; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class ImpAdjusterTest extends VertxTest { + + @Mock + private ImpValidator impValidator; + + @Mock + private BidderCatalog bidderCatalog; + + private ImpAdjuster target; + + private BidderAliases bidderAliases; + + @BeforeEach + public void setUp() { + target = new ImpAdjuster(jacksonMapper, new JsonMerger(jacksonMapper), impValidator); + bidderAliases = BidderAliases.of( + Map.of("someBidderAlias", "someBidder"), Collections.emptyMap(), bidderCatalog); + } + + @Test + public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpIsNull() { + // given + final Imp givenImp = Imp.builder().build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpIsAbsent() { + // given + final Imp givenImp = Imp.builder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode())) + .build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpDoesNotHaveRequestedBidder() { + // given + final Imp givenImp = Imp.builder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("imp", mapper.createObjectNode().set("anotherBidder", mapper.createObjectNode())))) + .build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpHasEmptyBidder() { + // given + final Imp givenImp = Imp.builder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("imp", mapper.createObjectNode().set("someBidder", mapper.createObjectNode())))) + .build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void adjustShouldReturnOriginalImpWhenMergedImpNodeIsEmpty() { + // given + final Imp givenImp = Imp.builder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() + .set("imp", mapper.createObjectNode().set("someBidder", mapper.createObjectNode())))) + .build(); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImp() throws ValidationException { + // given + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidder", givenBidderImp); + + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId2").build())).build()) + .bidfloor(new BigDecimal("2.0")) + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + verify(impValidator).validateImp(expectedImp); + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImpCaseInsensitive() throws ValidationException { + // given + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidder", givenBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "SOMEbiDDer", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId2").build())).build()) + .bidfloor(new BigDecimal("2.0")) + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + verify(impValidator).validateImp(expectedImp); + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImpCaseAliasBidder() throws ValidationException { + // given + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidderAlias", givenBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "SOMEbiDDer", bidderAliases, debugMessages); + + // then + final Imp expectedImp = givenImp.toBuilder() + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId2").build())).build()) + .bidfloor(new BigDecimal("2.0")) + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + verify(impValidator).validateImp(expectedImp); + assertThat(result).isEqualTo(expectedImp); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void resolveImpShouldReturnOriginalImpWhenResultingImpValidationFailed() throws ValidationException { + // given + doThrow(new ValidationException("imp validation failed")).when(impValidator).validateImp(any()); + + final ObjectNode givenBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .set("pmp", mapper.createObjectNode() + .set("deals", mapper.createArrayNode() + .add(mapper.createObjectNode().put("id", "dealId2")))); + + final Imp givenImp = givenImp("someBidder", givenBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).containsOnly( + "imp.ext.prebid.imp.someBidder can not be merged into original imp [id=impId], " + + "reason: imp validation failed"); + } + + @Test + public void resolveImpShouldReturnOriginalImpWhenMergingFailed() { + // given + final ObjectNode invalidBidderImp = mapper.createObjectNode() + .put("bidfloor", "2.0") + .put("pmp", 3); + + final Imp givenImp = givenImp("someBidder", invalidBidderImp); + final List debugMessages = new ArrayList<>(); + + // when + final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + + // then + assertThat(result).isSameAs(givenImp); + assertThat(debugMessages).hasSize(1).first() + .satisfies(message -> assertThat(message).startsWith( + "imp.ext.prebid.imp.someBidder can not be merged into original imp [id=impId]," + + " reason: Cannot construct instance of `com.iab.openrtb.request.Pmp`")); + } + + private static Imp givenImp(String bidder, ObjectNode bidderImpNode) { + final JsonNode givenExtPrebid = mapper.createObjectNode() + .put("prebidOriginAttr", "prebidOriginValue") + .set("imp", mapper.createObjectNode().set(bidder, bidderImpNode)); + + return Imp.builder() + .id("impId") + .tagid("impTagId") + .bidfloor(new BigDecimal("1.0")) + .bidfloorcur("USD") + .secure(1) + .pmp(Pmp.builder().deals(Collections.singletonList(Deal.builder().id("dealId").build())).build()) + .iframebuster(Collections.singletonList("iframebuster")) + .ext(mapper.createObjectNode().put("originAttr", "originValue").set("prebid", givenExtPrebid)) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/validation/ImpValidatorTest.java b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java new file mode 100644 index 00000000000..04e5eb1f326 --- /dev/null +++ b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java @@ -0,0 +1,2379 @@ +package org.prebid.server.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Asset; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.DataObject; +import com.iab.openrtb.request.Deal; +import com.iab.openrtb.request.EventTracker; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.ImageObject; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Metric; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Pmp; +import com.iab.openrtb.request.Request; +import com.iab.openrtb.request.TitleObject; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.request.VideoObject; +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.BidderCatalog; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; +import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class ImpValidatorTest extends VertxTest { + + private static final String RUBICON = "rubicon"; + + @Mock + private BidderParamValidator bidderParamValidator; + @Mock(strictness = LENIENT) + private BidderCatalog bidderCatalog; + + private ImpValidator target; + + @BeforeEach + public void setUp() { + target = new ImpValidator(bidderParamValidator, bidderCatalog, jacksonMapper); + + given(bidderCatalog.isValidName(RUBICON)).willReturn(true); + given(bidderCatalog.isActive(RUBICON)).willReturn(true); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpIdNull() { + // given + final List givenImps = singletonList(validImpBuilder().id(null).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpIdEmptyString() { + // given + final List givenImps = singletonList(validImpBuilder().id("").build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenNoneOfMediaTypeIsPresent() { + // given + final List givenImps = singletonList(validImpBuilder() + .video(null) + .audio(null) + .banner(null) + .xNative(null) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or " + + "\"native\""); + + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenVideoAttributeIsPresentButVideoMimesMissed() { + // given + final List givenImps = singletonList(validImpBuilder() + .video(Video.builder().mimes(emptyList()).build()) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].video.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenAudioAttributePresentButAudioMimesMissed() { + // given + final List givenImps = singletonList(validImpBuilder() + .audio(Audio.builder().mimes(emptyList()).build()) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].audio.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSizes() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAndValidSizes() + throws ValidationException { + + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(250) + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSizes() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasZeroHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldNotReturnValidationMessageForSizesIfImpIsInterstitial() throws ValidationException { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .instl(1) + .banner(Banner.builder() + .w(0) + .h(300) + .format(singletonList(Format.builder().w(1).h(1).build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasZeroWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasNegativeWidth() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerHasNegativeHeight() { + // given + final List givenImps = singletonList(Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatHWAndRatiosPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(2).wmin(3).wratio(4).hratio(5)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatHeightWeightAndOneOfRatiosPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(2).hratio(5)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosAndOneOfSizesPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).wmin(3).wratio(4).hratio(5)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpecifiedOnly() + throws ValidationException { + + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(2)); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpecifiedOnly() + throws ValidationException { + + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(3).wratio(4).hratio(5)); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatSizesAndRatiosPresent() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), identity()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] should define *either* {w, h} (for static size " + + "requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(null).w(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(0).w(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(null)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(1).w(0)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatHeightIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(-1).w(2)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatWidthIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.h(2).w(-1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(null).wratio(2).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define" + + " a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(0).wratio(2).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(-1).wratio(2).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(null).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\"," + + " and \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(0).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(-1).hratio(1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNull() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(5).hratio(null)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsZero() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(5).hratio(0)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNegative() { + // given + final List givenImps = overwriteBannerFormatInFirstImp(givenValidImps(), + formatBuilder -> formatBuilder.wmin(1).wratio(5).hratio(-1)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenPmpDealIdIsNull() { + // given + final List givenImps = overwritePmpFirstDealInFirstImp(givenValidImps(), + dealBuilder -> dealBuilder.id(null)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenPmpDealIdIsEmptyString() { + // given + final List givenImps = overwritePmpFirstDealInFirstImp(givenValidImps(), + dealBuilder -> dealBuilder.id("")); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpsShouldThrowExceptionWhenNativeRequestEmpty() { + // given + final List givenImps = givenImps(identity()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native contains empty request value"); + } + + @Test + public void validateImpsShouldThrowExceptionWhenNativeRequestMalformed() { + // given + final List givenImps = givenImps(nativeCustomizer -> nativeCustomizer.request("broken-request")); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessageStartingWith("Error while parsing request.imp[0].native.request: JsonParseException:"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithoutErrorsForNativeSpecificContextTypes() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(500).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenContextTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(323)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is invalid. " + + "See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenContextSubTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(2).contextsubtype(100)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.contextsubtype is invalid. " + + "See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleContentValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(2).contextsubtype(11)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is 2, but contextsubtype is 11. " + + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleSocialValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(3).contextsubtype(21)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is 3, but contextsubtype is 21. " + + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleProductValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(2).contextsubtype(31)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.context is 2, but contextsubtype is 31. " + + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenContextSubTypeAndContextTypeValid() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenContextIsNull() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(null).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenSubTypeContextIsNull() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(null).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenEventTrackersOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(323).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.eventtrackers[0].event is invalid. See section 7.6: " + + "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenEventTrackerEmptyMethods() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(1).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.eventtrackers[0].method is required. " + + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenEventTrackerInvalidMethod() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(1).methods(singletonList(3)).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.eventtrackers[0].methods[0] is invalid. " + + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenValidEventTracker() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(1).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithEmptyErrorWhenEventTrackerHasSpecificType() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() + .event(500).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithoutErrorsForNativeSpecificPlacementTypes() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.plcmttype(500).assets(singletonList(Asset.builder().build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenPlacementTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.plcmttype(323)); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.plcmttype is invalid. " + + "See https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenAssetsContainsZeroElements() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(emptyList())); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets must be an array containing at least one object"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenElementInAssetsHasWhichIsNotUnique() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(asList( + Asset.builder().id(1).build(), + // this should get ID set on second iteration (i = 1) and result in conflict with previous id + Asset.builder().build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[1].id is already being used by another asset. " + + "Each asset ID must be unique."); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndImage() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().build()) + .img(ImageObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0] must define at most one of" + + " {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndVideo() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().build()) + .video(VideoObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0] must define at most one of" + + " {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndData() + throws JsonProcessingException { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().build()) + .data(DataObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0] must define at most one of" + + " {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndVideo() + throws JsonProcessingException { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .img(ImageObject.builder().build()) + .video(VideoObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndData() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .img(ImageObject.builder().build()) + .data(DataObject.builder().build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage( + "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenHasZeroTitleLen() throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().len(0).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenHasNullTitleLen() throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .title(TitleObject.builder().len(null).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenDataTypeOutOfPossibleValuesRange() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .data(DataObject.builder().type(100).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].data.type is invalid. See section 7.4: " + + "https://iabtechlab.com/wp-content/uploads/2016/07/" + + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithoutErrorsWhenDataHasSpecicNativeTypes() + throws Exception { + + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .data(DataObject.builder().type(500).build()).build()))); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMimes() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder().mimes(emptyList()).build()).build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.mimes must be an array with at least one" + + " MIME type"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMinDuration() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(null) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasMinDurationLessThanOne() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(0) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMaxDuration() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(null) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasMaxDurationLessThanOne() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(0) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyProtocols() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(0) + .protocols(emptyList()) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnValidationResultWithErrorWhenNativeVideoProtocolsOutOfPossibleValues() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(0) + .protocols(singletonList(20)) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + } + + @Test + public void validateImpsShouldReturnEmptyValidationMessagesWhenNativeVideoIsValid() + throws JsonProcessingException { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(singletonList(Asset.builder() + .video(VideoObject.builder() + .mimes(singletonList("mime")) + .minduration(2) + .maxduration(2) + .protocols(singletonList(0)) + .build()) + .build()))); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].native.request.assets[0].video.protocols[0] must be in the range [1, 10]." + + " Got 0"); + } + + @Test + public void validateImpsShouldUpdateNativeRequestAssetsIds() throws Exception { + // given + final List givenImps = givenNativeImps(nativeReqCustomizer -> + nativeReqCustomizer.assets(asList(Asset.builder().build(), Asset.builder().build()))); + + // when + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + + assertThat(givenImps).hasSize(1) + .extracting(Imp::getXNative).doesNotContainNull() + .extracting(Native::getRequest).doesNotContainNull() + .extracting(req -> mapper.readValue(req, Request.class)) + .flatExtracting(Request::getAssets) + .flatExtracting(Asset::getId) + .containsOnly(0, 1); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() { + // given + final List givenImps = singletonList(validImpBuilder() + .metric(singletonList(Metric.builder().type(null).build())).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("Missing request.imp[0].metric[0].type"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenMetricValueIsNotValid() { + // given + final List givenImps = singletonList(validImpBuilder() + .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].metric[0].value must be in the range [0.0, 1.0]"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenNoImpExtPrebidPresent() { + // given + final List givenImps = singletonList(validImpBuilder().ext(null).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid must be defined"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidIsNotObject() { + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", "test"))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid must an object type"); + } + + @Test + public void validateImpsShouldReturnValidationMessagesWhenExtImpPrebidBidderWasNotDefined() { + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("attr", "value")))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.bidder must be defined"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidBiddersNotDefinedForStoredBidResponse() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .storedAuctionResponse(ExtStoredAuctionResponse.of("id", null)) + .build()); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.bidder should be defined for storedbidresponse"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseBidderMissed() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedbidresponse.bidder was not defined"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseIdMissed() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", null))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("Id was not defined for request.imp[0].ext.prebid.storedbidresponse.id"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotValidBidder() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + given(bidderCatalog.isValidName(eq("bidder"))).willReturn(false); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedbidresponse.bidder is not valid bidder"); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotInImpExtPrebidBidder() { + // given + final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() + .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) + .bidder(mapper.createObjectNode().put("rubicon", 1)) + .build()); + + given(bidderCatalog.isValidName(eq("bidder"))).willReturn(true); + + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>())) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedbidresponse.bidder does not have correspondent" + + " bidder parameters"); + } + + @Test + public void validateImpsShouldReturnEmptyMessagesWhenExtImpPrebidBidderWasMissedAndHasStoredAuctionResponseWas() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("storedauctionresponse", + mapper.createObjectNode().put("id", "1"))))).build()); + + // when & then + target.validateImps(givenImps, Collections.emptyMap(), new ArrayList<>()); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidBidderIsNotObject() { + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", "test")))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.bidder must be an object type"); + } + + @Test + public void validateImpsShouldReturnWarningAndDropBidderWhenImpExtPrebidBidderIsUnknown() + throws ValidationException { + + // given + final List givenImps = givenValidImps(); + given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages) + .containsExactly("WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder contains unknown bidder: rubicon", + "WARNING: request.imp[0].ext must contain at least one valid bidder"); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("bidder")) + .containsOnly(mapper.createObjectNode()); + } + + @Test + public void validateImpsShouldReturnWarningMessageAndDropBidderWhenBidderExtIsInvalid() throws ValidationException { + // given + final List givenImps = givenValidImps(); + given(bidderParamValidator.validate(any(), any())) + .willReturn(new LinkedHashSet<>(asList("errorMessage1", "errorMessage2"))); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages) + .containsExactly( + """ + WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: \ + request.imp[0].ext.prebid.bidder.rubicon failed validation. + errorMessage1 + errorMessage2""", + "WARNING: request.imp[0].ext must contain at least one valid bidder"); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("bidder")) + .containsOnly(mapper.createObjectNode()); + } + + @Test + public void validateImpsShouldReturnWarningMessageAndDropBidderWhenImpExtPrebidImpBidderIsUnknown() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", + Map.of("imp", singletonMap("unknownBidder", 0), "bidder", singletonMap("rubicon", 0))))) + .build()); + + given(bidderCatalog.isValidName(eq("unknownBidder"))).willReturn(false); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages).containsExactly( + "WARNING: request.imp[0].ext.prebid.imp.unknownBidder was dropped with the reason: invalid bidder"); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("imp")) + .containsOnly(mapper.createObjectNode()); + } + + @Test + public void validateImpsShouldReturnNoMessageWhenImpExtPrebidImpBidderIsAlias() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", + Map.of("imp", singletonMap("rubiconAlias", 0), "bidder", singletonMap("rubicon", 0))))) + .build()); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Map.of("rubiconAlias", "rubicon"), debugMessages); + + // then + assertThat(debugMessages).isEmpty(); + + assertThat(givenImps) + .extracting(Imp::getExt) + .extracting(impExt -> impExt.get("prebid")) + .extracting(prebid -> prebid.get("imp")) + .containsOnly(mapper.createObjectNode().put("rubiconAlias", 0)); + } + + @Test + public void validateImpsShouldReturnValidationMessageWhenExtImpPrebidHasStoredAuctionResponseWithoutId() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( + "storedauctionresponse", mapper.createObjectNode())))) + .build()); + + // when & then + assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) + .isInstanceOf(ValidationException.class) + .hasMessage("request.imp[0].ext.prebid.storedauctionresponse.id should be defined"); + } + + @Test + public void validateImpsShouldReturnWarningMessageWhenExtImpPrebidHasStoredAuctionResponseSeatBidArr() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", Map.of( + "storedauctionresponse", mapper.createObjectNode() + .put("id", "1") + .set("seatbidarr", mapper.createArrayNode()))) + )).build()); + + final List debugMessages = new ArrayList<>(); + + // when + target.validateImps(givenImps, Collections.emptyMap(), debugMessages); + + // then + assertThat(debugMessages) + .containsOnly("WARNING: request.imp[0].ext.prebid.storedauctionresponse.seatbidarr " + + "is not supported at the imp level"); + } + + // validateImp method tests + + @Test + public void validateImpShouldReturnValidationMessageWhenImpIdNull() { + // given + final Imp givenImp = validImpBuilder().id(null).build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=null] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasZeroHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenImpIdEmptyString() { + // given + final Imp givenImp = validImpBuilder().id("").build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenNoneOfMediaTypeIsPresent() { + // given + final Imp givenImp = validImpBuilder() + .video(null) + .audio(null) + .banner(null) + .xNative(null) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200] must contain at least one of \"banner\", \"video\", \"audio\", or " + + "\"native\""); + + } + + @Test + public void validateImpShouldReturnValidationMessageWhenVideoAttributeIsPresentButVideoMimesMissed() { + // given + final Imp givenImp = validImpBuilder() + .video(Video.builder().mimes(emptyList()).build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].video.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenAudioAttributePresentButAudioMimesMissed() { + // given + final Imp givenImp = validImpBuilder() + .audio(Audio.builder().mimes(emptyList()).build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].audio.mimes must contain at least one supported MIME type"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSizes() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAndValidSizes() + throws ValidationException { + + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .h(250) + .format(null) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSizes() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .w(300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldNotReturnValidationMessageForSizesIfImpIsInterstitial() throws ValidationException { + // given + final Imp givenImp = Imp.builder() + .id("11") + .instl(1) + .banner(Banner.builder() + .w(0) + .h(300) + .format(singletonList(Format.builder().w(1).h(1).build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasZeroWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(0) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasNegativeWidth() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(600) + .w(-300) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(emptyList()) + .build()) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerHasNegativeHeight() { + // given + final Imp givenImp = Imp.builder() + .id("11") + .banner(Banner.builder() + .h(-300) + .w(600) + .format(singletonList(Format.builder().build())) + .build()) + .ext(mapper.valueToTree( + singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=11].banner must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatHWAndRatiosPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(2).wmin(3).wratio(4).hratio(5).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatHeightWeightAndOneOfRatiosPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(2).hratio(5).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosAndOneOfSizesPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).wmin(3).wratio(4).hratio(5).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " + + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + } + + @Test + public void validateImpShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpecifiedOnly() + throws ValidationException { + + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(2).build())) + .build()) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpecifiedOnly() + throws ValidationException { + + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(3).wratio(4).hratio(5).build())) + .build()) + .build(); + + // when & then + target.validateImp(givenImp); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatSizesAndRatiosPresent() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] should define *either* {w, h} (for static size " + + "requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(null).w(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(0).w(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(null).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(1).w(0).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatHeightIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(-1).w(2).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatWidthIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().h(2).w(-1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"h\" and \"w\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(null).wratio(2).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define" + + " a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(0).wratio(2).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(-1).wratio(2).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define " + + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(null).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\"," + + " and \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(0).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(-1).hratio(1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and " + + "\"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNull() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(null).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsZero() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(0).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNegative() { + // given + final Imp givenImp = validImpBuilder() + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(-1).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].banner.format[0] must define a valid \"wmin\", \"wratio\", and" + + " \"hratio\" properties"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenPmpDealIdIsNull() { + // given + final Imp givenImp = validImpBuilder() + .pmp(Pmp.builder() + .deals(singletonList(Deal.builder().id(null).build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenPmpDealIdIsEmptyString() { + // given + final Imp givenImp = validImpBuilder() + .pmp(Pmp.builder() + .deals(singletonList(Deal.builder().id("").build())) + .build()) + .build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].pmp.deals[0] missing required field: \"id\""); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() { + // given + final Imp givenImp = validImpBuilder() + .metric(singletonList(Metric.builder().type(null).build())).build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("Missing imp[id=200].metric[0].type"); + } + + @Test + public void validateImpShouldReturnValidationMessageWhenMetricValueIsNotValid() { + // given + final Imp givenImp = validImpBuilder() + .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build(); + + // when & then + assertThatThrownBy(() -> target.validateImp(givenImp)) + .isInstanceOf(ValidationException.class) + .hasMessage("imp[id=200].metric[0].value must be in the range [0.0, 1.0]"); + } + + private static List givenImps(UnaryOperator nativeCustomizer) { + return singletonList(validImpBuilder().xNative(nativeCustomizer.apply(Native.builder()).build()).build()); + } + + private static List givenNativeImps(UnaryOperator nativeRequestCustomizer) + throws JsonProcessingException { + + return singletonList(validImpBuilder() + .xNative(Native.builder() + .request(mapper.writeValueAsString(nativeRequestCustomizer.apply( + Request.builder()).build())) + .build()) + .build()); + } + + private static List overwriteBannerFormatInFirstImp(List imps, + UnaryOperator formatModifier) { + + final Banner banner = imps.getFirst().getBanner().toBuilder() + .format(singletonList(formatModifier.apply(Format.builder()).build())).build(); + + return singletonList(validImpBuilder().banner(banner).build()); + } + + private static List overwritePmpFirstDealInFirstImp(List imps, + UnaryOperator dealModifier) { + + final Pmp pmp = imps.getFirst().getPmp().toBuilder() + .deals(singletonList(dealModifier.apply(dealModifier.apply(Deal.builder())).build())).build(); + + return singletonList(validImpBuilder().pmp(pmp).build()); + } + + private static List givenValidImps() { + return singletonList(validImpBuilder().build()); + } + + private static Imp.ImpBuilder validImpBuilder() { + return Imp.builder().id("200") + .video(Video.builder().mimes(singletonList("vmime")) + .build()) + .banner(Banner.builder() + .format(singletonList(Format.builder().wmin(1).wratio(5).hratio(1).build())) + .build()) + .pmp(Pmp.builder().deals(singletonList(Deal.builder().id("1").build())).build()) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))); + } + +} diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index b047a9ecde0..a195514fa70 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -1,39 +1,23 @@ package org.prebid.server.validation; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; -import com.iab.openrtb.request.App.AppBuilder; -import com.iab.openrtb.request.Asset; -import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.DataObject; import com.iab.openrtb.request.Deal; -import com.iab.openrtb.request.Deal.DealBuilder; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.EventTracker; import com.iab.openrtb.request.Format; -import com.iab.openrtb.request.Format.FormatBuilder; -import com.iab.openrtb.request.ImageObject; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Metric; -import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Regs; -import com.iab.openrtb.request.Request; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Site.SiteBuilder; -import com.iab.openrtb.request.TitleObject; import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.request.Video; -import com.iab.openrtb.request.VideoObject; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -45,7 +29,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -56,8 +39,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; -import org.prebid.server.proto.openrtb.ext.request.ExtStoredBidResponse; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; @@ -66,21 +47,20 @@ import java.math.BigDecimal; import java.util.Collections; import java.util.EnumMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.function.UnaryOperator; +import java.util.List; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; -import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -90,8 +70,8 @@ public class RequestValidatorTest extends VertxTest { @Mock(strictness = LENIENT) private BidderCatalog bidderCatalog; - @Mock(strictness = LENIENT) - private BidderParamValidator bidderParamValidator; + @Mock + private ImpValidator impValidator; @Mock private Metrics metrics; @@ -99,11 +79,10 @@ public class RequestValidatorTest extends VertxTest { @BeforeEach public void setUp() { - given(bidderParamValidator.validate(any(), any())).willReturn(Collections.emptySet()); given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(true); given(bidderCatalog.isActive(eq(RUBICON))).willReturn(true); - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, false); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, false); } @Test @@ -198,150 +177,110 @@ public void validateShouldReturnValidationMessageWhenNumberOfImpsIsZero() { } @Test - public void validateShouldReturnValidationMessageWhenImpIdNull() { + public void validateShouldReturnValidationMessageWhenAliasesKeyDoesntContainAliasgvlidsKey() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().id(null).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("pubmatic", "rubicon")) + .aliasgvlids(singletonMap("between", 2)) + .build())) .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0] missing required field: \"id\""); + assertThat(result.getErrors()) + .containsExactly("request.ext.prebid.aliasgvlids. vendorId 2 refers to unknown bidder alias: between"); } @Test - public void validateShouldReturnValidationMessageWhenImpIdEmptyString() { + public void validateShouldReturnValidationMessageWhenAliasgvlidsValueLowerThatOne() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().id("").build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("pubmatic", "rubicon")) + .aliasgvlids(singletonMap("pubmatic", 0)) + .build())) .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0] missing required field: \"id\""); + assertThat(result.getErrors()) + .containsExactly("request.ext.prebid.aliasgvlids. Invalid vendorId 0 for alias: pubmatic. " + + "Choose a different vendorId, or remove this entry."); } @Test - public void validateShouldReturnValidationMessageWhenNoneOfMediaTypeIsPresent() { + public void validateShouldReturnValidationMessageWhenSiteIdAndPageIsNull() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .video(null) - .audio(null) - .banner(null) - .xNative(null) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id(null).build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0] must contain at least one of \"banner\", \"video\", \"audio\", or " - + "\"native\""); + .containsOnly("request.site should include at least one of request.site.id or request.site.page"); } @Test - public void validateShouldReturnValidationMessageWhenVideoAttributeIsPresentButVideaMimesMissed() { + public void validateShouldReturnValidationMessageWhenSiteIdIsEmptyStringAndPageIsNull() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .video(Video.builder().mimes(emptyList()) - .build()) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].video.mimes must contain at least one supported MIME type"); + .containsOnly("request.site should include at least one of request.site.id or request.site.page"); } @Test - public void validateShouldReturnValidationMessageWhenAudioAttributePresentButAudioMimesMissed() { + public void validateShouldReturnEmptyValidationMessagesWhenPageIdIsNullAndSiteIdIsPresent() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .audio(Audio.builder().mimes(emptyList()) - .build()) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page(null).build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].audio.mimes must contain at least one supported MIME type"); + assertThat(result.hasErrors()).isFalse(); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasNullFormatAndNoSizes() { + public void validateShouldEmptyValidationMessagesWhenSitePageIsEmptyString() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .format(null) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page("").build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + assertThat(result.hasErrors()).isFalse(); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenBannerHasNullFormatAndValidSizes() { + public void validateShouldReturnValidationMessageWhenSiteIdAndPageBothEmpty() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .h(250) - .format(null) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").page("").build()).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.hasErrors()).isFalse(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.site should include at least one of request.site.id or request.site.page"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSizes() { + public void validateShouldReturnValidationMessageWhenSiteExtAmpIsNegative() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .site(Site.builder().id("id").page("page").ext(ExtSite.of(-1, null)).build()) .build(); // when @@ -349,23 +288,14 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoSi // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHeight() { + public void validateShouldReturnValidationMessageWhenSiteExtAmpIsGreaterThanOne() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .site(Site.builder().id("id").page("page").ext(ExtSite.of(2, null)).build()) .build(); // when @@ -373,73 +303,44 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoHe // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNoWidth() { + public void validateShouldFailWhenDoohIdAndVenuetypeAreNulls() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final Dooh invalidDooh = Dooh.builder().id(null).venuetype(null).build(); + final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroHeight() { + public void validateShouldFailWhenDoohIdIsNullAndVenuetypeIsEmpty() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .h(0) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); + final Dooh invalidDooh = Dooh.builder().id(null).venuetype(Collections.emptyList()).build(); + final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasZeroHeight() { + public void validateShouldReturnValidationMessageWhenRequestAppAndRequestSiteBothMissed() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .w(300) - .h(0) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .site(null) + .app(null) + .dooh(null) .build(); // when @@ -447,109 +348,84 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroHeight() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly("One of request.site or request.app or request.dooh must be defined"); } @Test - public void validateShouldNotReturnValidationMessageForSizesIfImpIsInterstitial() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .instl(1) - .banner(Banner.builder() - .w(0) - .h(300) - .format(singletonList(Format.builder().w(1).h(1).build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); - + public void validateShouldFailWhenDoohSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .dooh(Dooh.builder().build()) + .app(App.builder().build()) + .site(Site.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then - assertThat(result.getErrors()).isEmpty(); + verify(metrics).updateAlertsMetrics(MetricName.general); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.app and request.dooh and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenAliasesKeyDoesntContainAliasgvlidsKey() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("pubmatic", "rubicon")) - .aliasgvlids(singletonMap("between", 2)) - .build())) - .build(); - + public void validateShouldFailWhenSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .app(App.builder().build()) + .site(Site.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then - assertThat(result.getErrors()) - .containsExactly("request.ext.prebid.aliasgvlids. vendorId 2 refers to unknown bidder alias: between"); + verify(metrics).updateAlertsMetrics(MetricName.general); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.app and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenAliasgvlidsValueLowerThatOne() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("pubmatic", "rubicon")) - .aliasgvlids(singletonMap("pubmatic", 0)) - .build())) - .build(); - + public void validateShouldFailWhenDoohAndSiteArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .dooh(Dooh.builder().build()) + .site(Site.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then - assertThat(result.getErrors()) - .containsExactly("request.ext.prebid.aliasgvlids. Invalid vendorId 0 for alias: pubmatic. " - + "Choose a different vendorId, or remove this entry."); + verify(metrics).updateAlertsMetrics(MetricName.general); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.dooh and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndZeroWidth() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(0) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) - .build(); - + public void validateShouldFailWhenDoohAndAppArePresentInRequestAndStrictValidationIsEnabled() { // when - final ValidationResult result = target.validate(bidRequest, null); + target = new RequestValidator(bidderCatalog, impValidator, metrics, jacksonMapper, 0.01, true); + final BidRequest invalidRequest = validBidRequestBuilder() + .dooh(Dooh.builder().build()) + .app(App.builder().build()) + .build(); + final ValidationResult result = target.validate(invalidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.app and request.dooh and request.site are present, " + + "but no more than one of request.site or request.app or request.dooh can be defined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasZeroWidth() { + public void validateShouldReturnValidationMessageWhenMinWidthPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(0) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) + .build()) .build(); // when @@ -557,23 +433,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasZeroWidth() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeWidth() { + public void validateShouldReturnValidationMessageWhenMinWidthPercIsLessThanZero() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(-300) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) + .build()) .build(); // when @@ -581,24 +450,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasNegativeWidth() { + public void validateShouldReturnValidationMessageWhenMinWidthPercGreaterThanHundred() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(600) - .w(-300) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) + .build()) .build(); // when @@ -606,23 +467,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeWidth() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNegativeHeight() { + public void validateShouldReturnValidationMessageWhenMinHeightPercIsNull() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(-300) - .w(600) - .format(emptyList()) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) + .build()) .build(); // when @@ -631,23 +485,16 @@ public void validateShouldReturnValidationMessageWhenBannerHasEmptyFormatAndNega // then assertThat(result.getErrors()).hasSize(1) .containsOnly( - "request.imp[0].banner has no sizes. Define \"w\" and \"h\", or include \"format\" elements"); + "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerHasNegativeHeight() { + public void validateShouldReturnValidationMessageWhenMinHeightPercIsLessThanZero() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(Imp.builder() - .id("11") - .banner(Banner.builder() - .h(-300) - .w(600) - .format(singletonList(Format.builder().build())) - .build()) - .ext(mapper.valueToTree( - singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))) - .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) + .build()) .build(); // when @@ -655,59 +502,68 @@ public void validateShouldReturnValidationMessageWhenBannerHasNegativeHeight() { // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner must define a valid \"h\" and \"w\" properties"); + .containsOnly( + "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatHWAndRatiosPresent() { + public void validateShouldReturnValidationMessageWhenMinHeightPercGreaterThanHundred() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(2).wmin(3).wratio(4).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder() + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " - + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + .containsOnly( + "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatHeightWeightAndOneOfRatiosPresent() { + public void validateShouldReturnEmptyValidationMessagesWhenBidRequestIsOk() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(2).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder().build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " - + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosAndOneOfSizesPresent() { + public void validateShouldReturnValidationMessageWhenRequestHaveDuplicatedImpIds() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).wmin(3).wratio(4).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder() + .imp(asList(Imp.builder() + .id("11") + .build(), + Imp.builder() + .id("11") + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} *or* {wmin, wratio, " - + "hratio}, but not both. If both are valid, send two \"format\" objects in the request"); + .containsOnly("request.imp[0].id and request.imp[1].id are both \"11\". Imp IDs must be unique."); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpecifiedOnly() { + public void validateShouldNotReturnValidationMessageIfUserExtIsEmptyJsonObject() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(2)); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder().build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -717,10 +573,11 @@ public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatSizesSpec } @Test - public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpecifiedOnly() { + public void validateShouldNotReturnErrorMessageWhenRegsIsEmptyObject() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(3).wratio(4).hratio(5)); + final BidRequest bidRequest = validBidRequestBuilder() + .regs(Regs.builder().build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -730,1575 +587,32 @@ public void validateShouldReturnEmptyValidationMessagesWhenBannerFormatRatiosSpe } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatSizesAndRatiosPresent() { + public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoValues() { // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), identity()); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(emptyMap())) + .build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] should define *either* {w, h} (for static size " - + "requirements) *or* {wmin, wratio, hratio} (for flexible sizes) to be non-zero positive"); + .containsOnly("request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined." + + " If none exist, then request.user.ext.prebid should not be defined"); } @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(null).w(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndHeightIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(0).w(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(null)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatStaticSizesUsedAndWeightIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(1).w(0)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatHeightIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(-1).w(2)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatWidthIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().h(2).w(-1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"h\" and \"w\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(null).wratio(2).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define" - + " a valid \"wmin\", \"wratio\", and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(0).wratio(2).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define " - + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWMinIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(-1).wratio(2).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define " - + "a valid \"wmin\", \"wratio\", and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(null).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\"," - + " and \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(0).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " - + "\"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndWRatioIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(-1).hratio(1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and " - + "\"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNull() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(5).hratio(null)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" - + " \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsZero() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(5).hratio(0)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" - + " \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenBannerFormatRatiosUsedAndHRatioIsNegative() { - // given - final BidRequest bidRequest = overwriteBannerFormatInFirstImp(validBidRequestBuilder().build(), - formatBuilder -> Format.builder().wmin(1).wratio(5).hratio(-1)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Request imp[0].banner.format[0] must define a valid \"wmin\", \"wratio\", and" - + " \"hratio\" properties"); - } - - @Test - public void validateShouldReturnValidationMessageWhenPmpDealIdIsNull() { - // given - final BidRequest bidRequest = overwritePmpFirstDealInFirstImp(validBidRequestBuilder().build(), - dealBuilder -> Deal.builder().id(null)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].pmp.deals[0] missing required field: \"id\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenPmpDealIdIsEmptyString() { - // given - final BidRequest bidRequest = overwritePmpFirstDealInFirstImp(validBidRequestBuilder().build(), - dealBuilder -> Deal.builder().id("")); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].pmp.deals[0] missing required field: \"id\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteIdAndPageIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id(null).build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site should include at least one of request.site.id or request.site.page"); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteIdIsEmptyStringAndPageIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site should include at least one of request.site.id or request.site.page"); - } - - @Test - public void validateShouldReturnEmptyValidationMessagesWhenPageIdIsNullAndSiteIdIsPresent() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page(null).build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.hasErrors()).isFalse(); - } - - @Test - public void validateShouldEmptyValidationMessagesWhenSitePageIsEmptyString() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("1").page("").build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.hasErrors()).isFalse(); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteIdAndPageBothEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder().site(Site.builder().id("").page("").build()).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site should include at least one of request.site.id or request.site.page"); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteExtAmpIsNegative() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .site(Site.builder().id("id").page("page").ext(ExtSite.of(-1, null)).build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenSiteExtAmpIsGreaterThanOne() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .site(Site.builder().id("id").page("page").ext(ExtSite.of(2, null)).build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.site.ext.amp must be either 1, 0, or undefined"); - } - - @Test - public void validateShouldFailWhenDoohIdAndVenuetypeAreNulls() { - // given - final Dooh invalidDooh = Dooh.builder().id(null).venuetype(null).build(); - final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); - } - - @Test - public void validateShouldFailWhenDoohIdIsNullAndVenuetypeIsEmpty() { - // given - final Dooh invalidDooh = Dooh.builder().id(null).venuetype(Collections.emptyList()).build(); - final BidRequest bidRequest = validBidRequestBuilder().site(null).dooh(invalidDooh).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."); - } - - @Test - public void validateShouldReturnValidationMessageWhenRequestAppAndRequestSiteBothMissed() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .site(null) - .app(null) - .dooh(null) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("One of request.site or request.app or request.dooh must be defined"); - } - - @Test - public void validateShouldFailWhenDoohSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .dooh(Dooh.builder().build()) - .app(App.builder().build()) - .site(Site.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - verify(metrics).updateAlertsMetrics(MetricName.general); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.app and request.dooh and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldFailWhenSiteAndAppArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .app(App.builder().build()) - .site(Site.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - verify(metrics).updateAlertsMetrics(MetricName.general); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.app and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldFailWhenDoohAndSiteArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .dooh(Dooh.builder().build()) - .site(Site.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - verify(metrics).updateAlertsMetrics(MetricName.general); - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.dooh and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldFailWhenDoohAndAppArePresentInRequestAndStrictValidationIsEnabled() { - // when - target = new RequestValidator(bidderCatalog, bidderParamValidator, metrics, jacksonMapper, 0.01, true); - final BidRequest invalidRequest = validBidRequestBuilder() - .dooh(Dooh.builder().build()) - .app(App.builder().build()) - .build(); - final ValidationResult result = target.validate(invalidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.app and request.dooh and request.site are present, " - + "but no more than one of request.site or request.app or request.dooh can be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinWidthPercIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(null, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinWidthPercIsLessThanZero() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(-1, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinWidthPercGreaterThanHundred() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(101, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinHeightPercIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, null)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinHeightPercIsLessThanZero() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, -1)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMinHeightPercGreaterThanHundred() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .device(Device.builder() - .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(50, 101)))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"); - } - - @Test - public void validateShouldReturnEmptyValidationMessagesWhenBidRequestIsOk() { - // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenNoImpExtPrebidPresent() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().ext(null).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid must be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenImpExtPrebidIsNotObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder().ext(mapper.valueToTree(singletonMap("prebid", "test"))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid must an object type"); - } - - @Test - public void validateShouldReturnValidationMessagesWhenExtImpPrebidBidderWasNotDefined() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("attr", "value")))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.bidder must be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenImpExtPrebidBiddersNotDefinedForStoredBidResponse() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .storedAuctionResponse(ExtStoredAuctionResponse.of("id", null)) - .build()); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.bidder should be defined for storedbidresponse"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderMissed() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of(null, "id"))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder was not defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseIdMissed() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", null))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Id was not defined for request.imp[0].ext.prebid.storedbidresponse.id"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotValidBidder() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - given(bidderCatalog.isValidName(eq("bidder"))).willReturn(false); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder is not valid bidder"); - } - - @Test - public void validateShouldReturnValidationMessageWhenStoredBidResponseBidderIsNotInImpExtPrebidBidder() { - // given - final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() - .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .bidder(mapper.createObjectNode().put("rubicon", 1)) - .build()); - - given(bidderCatalog.isValidName(eq("bidder"))).willReturn(true); - - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", prebid))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.storedbidresponse.bidder does not have correspondent" - + " bidder parameters"); - } - - @Test - public void validateShouldReturnEmptyMessagesWhenExtImpPrebidBidderWasMissedAndHasStoredAuctionResponseWas() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("storedauctionresponse", - mapper.createObjectNode().put("id", "1"))))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenExtImpPrebidHasStoredAuctionResponseWithoutId() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( - "storedauctionresponse", mapper.createObjectNode())))).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()) - .containsOnly("request.imp[0].ext.prebid.storedauctionresponse.id should be defined"); - assertThat(result.getWarnings()).isEmpty(); - } - - @Test - public void validateShouldReturnWarningMessageWhenExtImpPrebidHasStoredAuctionResponseSeatBidArr() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", Map.of( - "storedauctionresponse", mapper.createObjectNode() - .put("id", "1") - .set("seatbidarr", mapper.createArrayNode()))) - )).build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getWarnings()) - .containsOnly("WARNING: request.imp[0].ext.prebid.storedauctionresponse.seatbidarr " - + "is not supported at the imp level"); - } - - @Test - public void validateShouldReturnValidationMessageWhenImpExtPrebidBidderIsNotObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", "test")))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].ext.prebid.bidder must be an object type"); - } - - @Test - public void validateShouldReturnWarningAndDropBidderWhenImpExtPrebidBidderIsUnknown() { - // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - given(bidderCatalog.isValidName(eq(RUBICON))).willReturn(false); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getWarnings()).hasSize(2) - .containsOnly("WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: " - + "request.imp[0].ext.prebid.bidder contains unknown bidder: rubicon", - "WARNING: request.imp[0].ext must contain at least one valid bidder"); - assertThat(bidRequest.getImp()) - .extracting(Imp::getExt) - .extracting(impExt -> impExt.get("prebid")) - .extracting(prebid -> prebid.get("bidder")) - .containsOnly(mapper.createObjectNode()); - } - - @Test - public void validateShouldReturnWarningMessageAndDropBidderWhenBidderExtIsInvalid() { - // given - final BidRequest bidRequest = validBidRequestBuilder().build(); - given(bidderParamValidator.validate(any(), any())) - .willReturn(new LinkedHashSet<>(asList("errorMessage1", "errorMessage2"))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getWarnings()) - .containsExactly( - """ - WARNING: request.imp[0].ext.prebid.bidder.rubicon was dropped with a reason: \ - request.imp[0].ext.prebid.bidder.rubicon failed validation. - errorMessage1 - errorMessage2""", - "WARNING: request.imp[0].ext must contain at least one valid bidder"); - assertThat(bidRequest.getImp()) - .extracting(Imp::getExt) - .extracting(impExt -> impExt.get("prebid")) - .extracting(prebid -> prebid.get("bidder")) - .containsOnly(mapper.createObjectNode()); - } - - @Test - public void validateShouldNotReturnValidationMessageIfUserExtIsEmptyJsonObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder().build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnErrorMessageWhenRegsIsEmptyObject() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .regs(Regs.builder().build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsNoValues() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(emptyMap())) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.ext.prebid requires a \"buyeruids\" property with at least one ID defined." - + " If none exist, then request.user.ext.prebid should not be defined"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsHasNullElement() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, singletonList(null))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidpermissions[i] can't be null"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsNull() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList(ExtRequestPrebidDataEidPermissions.of("source", null)))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" - + " null"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList(ExtRequestPrebidDataEidPermissions.of("source", emptyList())))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" - + " null"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderIsNotRecognizedBidder() { - // given - given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(false); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: 'bidder1'"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderHasBlankValue() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList(" "))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: ' '"); - } - - @Test - public void validateShouldNotReturnValidationErrorWhenBidderIsAlias() { - // given - given(bidderCatalog.isValidName(eq("bidder1Alias"))).willReturn(false); - given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(true); - given(bidderCatalog.isActive(eq("bidder1"))).willReturn(true); - - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("bidder1Alias", "bidder1")) - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnValidationErrorWhenBidderIsAsterisk() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSource() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .data(ExtRequestPrebidData.of(null, - singletonList( - ExtRequestPrebidDataEidPermissions.of(null, singletonList("bidder1"))))) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Missing required value request.ext.prebid.data.eidPermissions[].source"); - } - - @Test - public void validateShouldReturnValidationMessageWhenCantParseTargetingPriceGranularity() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(new TextNode("pricegranularity")) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Error while parsing request.ext.prebid.targeting.pricegranularity"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesAreEmptyList() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2, emptyList()))) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: empty granularity definition supplied"); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsZero() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity - .of(2, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), - BigDecimal.valueOf(0)))))) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsMissed() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( - 2, - singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), null))))) - .build()) - .build())) - .build(); - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsNegative() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( - 2, - singletonList(ExtGranularityRange.of( - BigDecimal.valueOf(5), BigDecimal.valueOf(-1)))))) - .build()) - .build())) - .build(); - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenPrecisionIsNegative() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(-1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))))) - .build()) - .build())) - .build(); - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: precision must be non-negative"); - } - - @Test - public void validateShouldReturnValidationMessageWhenMediaTypePriceGranularityTypesAreAllNull() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); - - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .mediatypepricegranularity(ExtMediaTypePriceGranularity.of(null, null, null)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Media type price granularity error: must have at least one media type present"); - } - - @Test - public void validateShouldReturnValidationMessageWithCorrectMediaType() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); - final ExtMediaTypePriceGranularity mediaTypePriceGranuality = ExtMediaTypePriceGranularity.of( - mapper.valueToTree(ExtPriceGranularity.of( - -1, - singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(1))))), - null, - null); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .mediatypepricegranularity(mediaTypePriceGranuality) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Banner price granularity error: precision must be non-negative"); - } - - @Test - public void validateShouldReturnValidationMessageForInvalidTargetingPrefix() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( - ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); - final String prefix = "1234567890"; - final int truncateattrchars = 10; - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .includebidderkeys(true) - .includewinners(true) - .truncateattrchars(truncateattrchars) - .prefix(prefix) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("ext.prebid.targeting: decrease prefix length or increase truncateattrchars" - + " by " + (prefix.length() + 11 - truncateattrchars) + " characters"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesContainsMissedMaxValue() { - final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(null, BigDecimal.valueOf(0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranuality)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: max value should not be missed"); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValue() { - final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(BigDecimal.valueOf(2), BigDecimal.valueOf(0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranuality)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValueInTheMiddleOfRangeList() { - // given - final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(0.05)), - ExtGranularityRange.of(BigDecimal.valueOf(8), BigDecimal.valueOf(0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranuality)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenIncrementIsNegativeInNotLeadingElement() { - // given - final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(2, - asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), - ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(-0.05)))); - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .targeting(ExtRequestTargeting.builder() - .pricegranularity(mapper.valueToTree(priceGranularity)) - .build()) - .build())) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("Price granularity error: increment must be a nonzero positive number"); - } - - @Test - public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsUnknownBidder() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.ext.unknown-bidder is neither a known bidder name " - + "nor an alias in request.ext.prebid.aliases"); - } - - @Test - public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidderAlias() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("unknown-bidder", "rubicon")) - .build())) - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidder() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .ext(ExtUser.builder() - .prebid(ExtUserPrebid.of(singletonMap("rubicon", "42"))) - .build()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldNotReturnValidationMessageWhenEidsIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(emptyList()) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidHasEmptySource() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of(null, null, null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0] missing required field: \"source\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidHasNoUids() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of("source", null, null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids must contain at least one element"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidUidsIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of("source", emptyList(), null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids must contain at least one element"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of( - "source", - singletonList(Uid.of(null, null, null)), - null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids[0] missing required field: \"id\""); - } - - @Test - public void validateShouldReturnValidationMessageWhenAliasNameEqualsToBidderItPointsOn() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("rubicon", "rubicon")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly(""" - request.ext.prebid.aliases.rubicon defines a no-op alias. \ - Choose a different alias, or remove this entry"""); - } - - @Test - public void validateShouldReturnValidationMessageWhenAliasPointOnNotValidBidderName() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("alias", "fake")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.aliases.alias refers to unknown bidder: fake"); - } - - @Test - public void validateShouldReturnValidationMessageWhenAliasPointOnDisabledBidder() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("alias", "appnexus")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - given(bidderCatalog.isValidName("appnexus")).willReturn(true); - given(bidderCatalog.isActive("appnexus")).willReturn(false); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.ext.prebid.aliases.alias refers to disabled bidder: appnexus"); - } - - @Test - public void validateShouldReturnEmptyValidationMessagesWhenAliasesWasUsed() { - // given - final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() - .aliases(singletonMap("alias", "rubicon")) - .build()); - final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithErrorsWhenGdprIsNotOneOrZero() { + public void validateShouldReturnValidationMessageWhenEidsPermissionsHasNullElement() { // given final BidRequest bidRequest = validBidRequestBuilder() - .regs(Regs.builder().gdpr(2).build()) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, singletonList(null))) + .build())) .build(); // when @@ -2306,157 +620,102 @@ public void validateShouldReturnValidationResultWithErrorsWhenGdprIsNotOneOrZero // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.regs.ext.gdpr must be either 0 or 1"); - } - - @Test - public void validateShouldThrowExceptionWhenNativeRequestEmpty() { - // given - final BidRequest bidRequest = givenBidRequest(identity()); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native contains empty request value"); - } - - @Test - public void validateShouldThrowExceptionWhenNativeRequestMalformed() { - // given - final BidRequest bidRequest = givenBidRequest(nativeCustomizer -> nativeCustomizer.request("broken-request")); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error) - .startsWith("Error while parsing request.imp[0].native.request: JsonParseException:"); - }); - } - - @Test - public void validateShouldReturnValidationResultWithoutErrorsForNativeSpecificContextTypes() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(500).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithErrorWhenContextTypeOutOfPossibleValuesRange() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(323)); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is invalid. " - + "See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidpermissions[i] can't be null"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenContextSubTypeOutOfPossibleValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsNull() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(2).contextsubtype(100)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList(ExtRequestPrebidDataEidPermissions.of("source", null)))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.contextsubtype is invalid. " - + "See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" + + " null"); } @Test - public void validateShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleContentValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBiddersIsEmpty() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(2).contextsubtype(11)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList(ExtRequestPrebidDataEidPermissions.of("source", emptyList())))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is 2, but contextsubtype is 11. " - + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidpermissions[].bidders[] required values but was empty or" + + " null"); } @Test - public void validateShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleSocialValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderIsNotRecognizedBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(3).contextsubtype(21)); + given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(false); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is 3, but contextsubtype is 21. " - + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly( + "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: 'bidder1'"); } @Test - public void validateShouldReturnErrorWhenContextSubTypeAndContextTypeOutOfPossibleProductValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsBidderHasBlankValue() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(2).contextsubtype(31)); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList(" "))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.context is 2, but contextsubtype is 31. " - + "This is an invalid combination. See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=39"); + .containsOnly("request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: ' '"); } @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenContextSubTypeAndContextTypeValid() - throws JsonProcessingException { + public void validateShouldNotReturnValidationErrorWhenBidderIsAlias() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } + given(bidderCatalog.isValidName(eq("bidder1Alias"))).willReturn(false); + given(bidderCatalog.isValidName(eq("bidder1"))).willReturn(true); + given(bidderCatalog.isActive(eq("bidder1"))).willReturn(true); - @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenContextIsNull() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(null).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("bidder1Alias", "bidder1")) + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("bidder1"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -2466,11 +725,15 @@ public void validateShouldReturnValidationResultWithEmptyErrorWhenContextIsNull( } @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenSubTypeContextIsNull() - throws JsonProcessingException { + public void validateShouldNotReturnValidationErrorWhenBidderIsAsterisk() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(null).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of("source", singletonList("*"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -2480,306 +743,342 @@ public void validateShouldReturnValidationResultWithEmptyErrorWhenSubTypeContext } @Test - public void validateShouldReturnValidationResultWithErrorWhenEventTrackersOutOfPossibleValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidsPermissionsHasMissingSource() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(323).build())).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .data(ExtRequestPrebidData.of(null, + singletonList( + ExtRequestPrebidDataEidPermissions.of(null, singletonList("bidder1"))))) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.eventtrackers[0].event is invalid. See section 7.6: " - + "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + .containsOnly("Missing required value request.ext.prebid.data.eidPermissions[].source"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenEventTrackerEmptyMethods() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenCantParseTargetingPriceGranularity() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(1).build())).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(new TextNode("pricegranularity")) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.eventtrackers[0].method is required. " - + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); + .containsOnly("Error while parsing request.ext.prebid.targeting.pricegranularity"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenEventTrackerInvalidMethod() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenRangesAreEmptyList() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(1).methods(singletonList(3)).build())).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(2, emptyList()))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.eventtrackers[0].methods[0] is invalid. " - + "See section 7.7: https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=43"); - } - - @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenValidEventTracker() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(1).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void validateShouldReturnValidationResultWithEmptyErrorWhenEventTrackerHasSpecificType() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.context(1).contextsubtype(12).eventtrackers(singletonList(EventTracker.builder() - .event(500).methods(singletonList(2)).build())).assets(singletonList(Asset.builder().build()))); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).isEmpty(); + .containsOnly("Price granularity error: empty granularity definition supplied"); } @Test - public void validateShouldReturnValidationResultWithoutErrorsForNativeSpecificPlacementTypes() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsZero() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.plcmttype(500).assets(singletonList(Asset.builder().build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity + .of(2, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), + BigDecimal.valueOf(0)))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenPlacementTypeOutOfPossibleValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsMissed() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.plcmttype(323)); - + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( + 2, + singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), null))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.plcmttype is invalid. " - + "See https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenAssetsContainsZeroElements() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsNegative() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(emptyList())); - + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of( + 2, + singletonList(ExtGranularityRange.of( + BigDecimal.valueOf(5), BigDecimal.valueOf(-1)))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets must be an array containing at least one object"); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenElementInAssetsHasWhichIsNotUnique() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenPrecisionIsNegative() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(asList( - Asset.builder().id(1).build(), - // this should get ID set on second iteration (i = 1) and result in conflict with previous id - Asset.builder().build()))); - + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(ExtPriceGranularity.of(-1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))))) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[1].id is already being used by another asset. " - + "Each asset ID must be unique."); + .containsOnly("Price granularity error: precision must be non-negative"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndImage() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenMediaTypePriceGranularityTypesAreAllNull() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().build()) - .img(ImageObject.builder().build()) - .build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .mediatypepricegranularity(ExtMediaTypePriceGranularity.of(null, null, null)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0] must define at most one of" - + " {title, img, video, data}"); + .containsOnly("Media type price granularity error: must have at least one media type present"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndVideo() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWithCorrectMediaType() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().build()) - .video(VideoObject.builder().build()) - .build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + final ExtMediaTypePriceGranularity mediaTypePriceGranuality = ExtMediaTypePriceGranularity.of( + mapper.valueToTree(ExtPriceGranularity.of( + -1, + singletonList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(1))))), + null, + null); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .mediatypepricegranularity(mediaTypePriceGranuality) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0] must define at most one of" - + " {title, img, video, data}"); + .containsOnly("Banner price granularity error: precision must be non-negative"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasTitleAndData() - throws JsonProcessingException { - + public void validateShouldReturnValidationMessageForInvalidTargetingPrefix() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().build()) - .data(DataObject.builder().build()) - .build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + final String prefix = "1234567890"; + final int truncateattrchars = 10; + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .includebidderkeys(true) + .includewinners(true) + .truncateattrchars(truncateattrchars) + .prefix(prefix) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0] must define at most one of" - + " {title, img, video, data}"); + .containsOnly("ext.prebid.targeting: decrease prefix length or increase truncateattrchars" + + " by " + (prefix.length() + 11 - truncateattrchars) + " characters"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndVideo() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .img(ImageObject.builder().build()) - .video(VideoObject.builder().build()) - .build()))); + public void validateShouldReturnValidationMessageWhenRangesContainsMissedMaxValue() { + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(null, BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + .containsOnly("Price granularity error: max value should not be missed"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenIndividualAssetHasImageAndData() - throws JsonProcessingException { - // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .img(ImageObject.builder().build()) - .data(DataObject.builder().build()) - .build()))); + public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValue() { + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(BigDecimal.valueOf(2), BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0] must define at most one of {title, img, video, data}"); + .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); } @Test - public void validateShouldReturnValidationResultWithErrorWhenHasZeroTitleLen() throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenRangesAreNotOrderedByMaxValueInTheMiddleOfRangeList() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().len(0).build()).build()))); + final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(0.05)), + ExtGranularityRange.of(BigDecimal.valueOf(8), BigDecimal.valueOf(0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranuality)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + .containsOnly("Price granularity error: range list must be ordered with increasing \"max\""); } @Test - public void validateShouldReturnValidationResultWithErrorWhenHasNullTitleLen() throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenIncrementIsNegativeInNotLeadingElement() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .title(TitleObject.builder().len(null).build()).build()))); + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(2, + asList(ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)), + ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(-0.05)))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .build()) + .build())) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].title.len must be a positive integer"); + .containsOnly("Price granularity error: increment must be a nonzero positive number"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenDataTypeOutOfPossibleValuesRange() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenPrebidBuyerIdsContainsUnknownBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .data(DataObject.builder().type(100).build()).build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) + .build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0].data.type is invalid. See section 7.4: " - + "https://iabtechlab.com/wp-content/uploads/2016/07/" - + "OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40"); + .containsOnly("request.user.ext.unknown-bidder is neither a known bidder name " + + "nor an alias in request.ext.prebid.aliases"); } @Test - public void validateShouldReturnValidationResultWithoutErrorsWhenDataHasSpecicNativeTypes() - throws JsonProcessingException { + public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidderAlias() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .data(DataObject.builder().type(500).build()).build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("unknown-bidder", "rubicon")) + .build())) + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(singletonMap("unknown-bidder", "42"))) + .build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); @@ -2789,213 +1088,182 @@ public void validateShouldReturnValidationResultWithoutErrorsWhenDataHasSpecicNa } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMimes() - throws JsonProcessingException { + public void validateShouldNotReturnAnyErrorInValidationResultWhenPrebidBuyerIdIsKnownBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder().mimes(emptyList()).build()).build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .ext(ExtUser.builder() + .prebid(ExtUserPrebid.of(singletonMap("rubicon", "42"))) + .build()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0].video.mimes must be an array with at least one" - + " MIME type"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMinDuration() - throws JsonProcessingException { + public void validateShouldNotReturnValidationMessageWhenEidsIsEmpty() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(null) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(emptyList()) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasMinDurationLessThanOne() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidHasEmptySource() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(0) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(singletonList(Eid.of(null, null, null))) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.minduration must be a positive integer"); + .containsOnly("request.user.eids[0] missing required field: \"source\""); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyMaxDuration() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidHasNoUids() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(null) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(singletonList(Eid.of("source", null, null))) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + .containsOnly("request.user.eids[0].uids must contain at least one element"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasMaxDurationLessThanOne() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidUidsIsEmpty() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(0) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(singletonList(Eid.of("source", emptyList(), null))) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + .containsOnly("request.user.eids[0].uids must contain at least one element"); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoHasEmptyProtocols() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(0) - .protocols(emptyList()) - .build()) - .build()))); + final BidRequest bidRequest = validBidRequestBuilder() + .user(User.builder() + .eids(singletonList(Eid.of( + "source", + singletonList(Uid.of(null, null, null)), + null))) + .build()) + .build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + .containsOnly("request.user.eids[0].uids[0] missing required field: \"id\""); } @Test - public void validateShouldReturnValidationResultWithErrorWhenNativeVideoProtocolsOutOfPossibleValues() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenAliasNameEqualsToBidderItPointsOn() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(0) - .protocols(singletonList(20)) - .build()) - .build()))); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("rubicon", "rubicon")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].native.request.assets[0].video.maxduration must be a positive integer"); + .containsOnly(""" + request.ext.prebid.aliases.rubicon defines a no-op alias. \ + Choose a different alias, or remove this entry"""); } @Test - public void validateShouldReturnEmptyValidationMessagesWhenNativeVideoIsValid() - throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenAliasPointOnNotValidBidderName() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(singletonList(Asset.builder() - .video(VideoObject.builder() - .mimes(singletonList("mime")) - .minduration(2) - .maxduration(2) - .protocols(singletonList(0)) - .build()) - .build()))); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "fake")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then assertThat(result.getErrors()).hasSize(1) - .containsOnly( - "request.imp[0].native.request.assets[0].video.protocols[0] must be in the range [1, 10]." - + " Got 0"); + .containsOnly("request.ext.prebid.aliases.alias refers to unknown bidder: fake"); } @Test - public void validateShouldUpdateNativeRequestAssetsIds() throws JsonProcessingException { + public void validateShouldReturnValidationMessageWhenAliasPointOnDisabledBidder() { // given - final BidRequest bidRequest = givenBidRequestWithNativeRequest(nativeReqCustomizer -> - nativeReqCustomizer.assets(asList(Asset.builder().build(), Asset.builder().build()))); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "appnexus")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); + given(bidderCatalog.isValidName("appnexus")).willReturn(true); + given(bidderCatalog.isActive("appnexus")).willReturn(false); // when - target.validate(bidRequest, null); + final ValidationResult result = target.validate(bidRequest, null); - assertThat(bidRequest.getImp()).hasSize(1) - .extracting(Imp::getXNative).doesNotContainNull() - .extracting(Native::getRequest).doesNotContainNull() - .extracting(req -> mapper.readValue(req, Request.class)) - .flatExtracting(Request::getAssets) - .flatExtracting(Asset::getId) - .containsOnly(0, 1); + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("request.ext.prebid.aliases.alias refers to disabled bidder: appnexus"); } @Test - public void validateShouldReturnValidationMessageWhenMetricTypeNullOrEmpty() { + public void validateShouldReturnEmptyValidationMessagesWhenAliasesWasUsed() { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .metric(singletonList(Metric.builder().type(null).build())).build())) - .build(); + final ExtRequest ext = ExtRequest.of(ExtRequestPrebid.builder() + .aliases(singletonMap("alias", "rubicon")) + .build()); + final BidRequest bidRequest = validBidRequestBuilder().ext(ext).build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .element(0).isEqualTo("Missing request.imp[0].metric[0].type"); + assertThat(result.getErrors()).isEmpty(); } @Test - public void validateShouldReturnValidationMessageWhenMetricValueIsNotValid() { + public void validateShouldReturnValidationResultWithErrorsWhenGdprIsNotOneOrZero() { // given final BidRequest bidRequest = validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .metric(singletonList(Metric.builder().type("viewability").value(2.0f).build())).build())) + .regs(Regs.builder().gdpr(2).build()) .build(); // when @@ -3003,7 +1271,7 @@ public void validateShouldReturnValidationMessageWhenMetricValueIsNotValid() { // then assertThat(result.getErrors()).hasSize(1) - .element(0).isEqualTo("request.imp[0].metric[0].value must be in the range [0.0, 1.0]"); + .containsOnly("request.regs.ext.gdpr must be either 0 or 1"); } @Test @@ -3182,43 +1450,33 @@ public void validateShouldReturnValidationMessageWhenMultipleSchainsForSameBidde } @Test - public void validateShouldReturnValidationMessageWhenRequestHaveDuplicatedImpIds() { + public void validateShouldReturnValidationMessageWhenImpValidationFailed() throws ValidationException { // given - final BidRequest bidRequest = validBidRequestBuilder() - .imp(asList(Imp.builder() - .id("11") - .build(), - Imp.builder() - .id("11") - .build())) - .build(); + doThrow(new ValidationException("imp[0] validation failed")) + .when(impValidator).validateImps(any(), any(), any()); + + final BidRequest bidRequest = validBidRequestBuilder().build(); // when final ValidationResult result = target.validate(bidRequest, null); // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.imp[0].id and request.imp[1].id are both \"11\". Imp IDs must be unique."); + assertThat(result.getErrors()).containsOnly("imp[0] validation failed"); } - private static BidRequest givenBidRequest( - UnaryOperator nativeCustomizer) { - return validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .xNative(nativeCustomizer.apply(Native.builder()).build()).build())).build(); - } + @Test + public void validateShouldReturnWarningMessageWhenImpValidationWarns() throws ValidationException { + // given + doAnswer(invocation -> ((List) invocation.getArgument(2)).add("imp[0] validation warning")) + .when(impValidator).validateImps(any(), any(), any()); - private static BidRequest givenBidRequestWithNativeRequest( - UnaryOperator nativeRequestCustomizer) - throws JsonProcessingException { - return validBidRequestBuilder() - .imp(singletonList(validImpBuilder() - .xNative(Native.builder() - .request(mapper.writeValueAsString(nativeRequestCustomizer.apply( - Request.builder()).build())) - .build()) - .build())) - .build(); + final BidRequest bidRequest = validBidRequestBuilder().build(); + + // when + final ValidationResult result = target.validate(bidRequest, null); + + // then + assertThat(result.getWarnings()).containsOnly("imp[0] validation warning"); } private static BidRequest.BidRequestBuilder validBidRequestBuilder() { @@ -3239,28 +1497,4 @@ private static Imp.ImpBuilder validImpBuilder() { .ext(mapper.valueToTree(singletonMap("prebid", singletonMap("bidder", singletonMap("rubicon", 0))))); } - private static BidRequest overwriteBannerFormatInFirstImp( - BidRequest bidRequest, UnaryOperator formatModifier) { - final Banner banner = bidRequest.getImp().getFirst().getBanner().toBuilder() - .format(singletonList(formatModifier.apply(Format.builder()).build())).build(); - - return bidRequest.toBuilder().imp(singletonList(validImpBuilder().banner(banner).build())).build(); - } - - private static BidRequest overwritePmpFirstDealInFirstImp( - BidRequest bidRequest, UnaryOperator dealModifier) { - final Pmp pmp = bidRequest.getImp().getFirst().getPmp().toBuilder() - .deals(singletonList(dealModifier.apply(dealModifier.apply(Deal.builder())).build())).build(); - - return bidRequest.toBuilder().imp(singletonList(validImpBuilder().pmp(pmp).build())).build(); - } - - private static BidRequest.BidRequestBuilder requestWithBothSiteAndApp( - BidRequest.BidRequestBuilder builder, - UnaryOperator siteModifier, - UnaryOperator appModifier) { - - return builder.site(siteModifier.apply(Site.builder()).build()) - .app(appModifier.apply(App.builder()).build()); - } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json index dfa745e2d1d..a0d9014043a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json @@ -11,6 +11,22 @@ "h": 250 }, "ext": { + "prebid": { + "imp": { + "generic": { + "pmp": { + "deals": [ + { + "id": "dealId" + } + ] + }, + "ext": { + "someExt": "someExt" + } + } + } + }, "generic": { "accountId": 2001, "siteId": 3001, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json index 697daed2ae1..d1ae2f0fce0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json @@ -1,20 +1,30 @@ { "id" : "tid", "imp" : [ { - "id" : "impId001", - "video" : { - "mimes" : [ "mimes" ], - "w" : 300, - "h" : 250 + "id": "impId001", + "video": { + "mimes": [ + "mimes" + ], + "w": 300, + "h": 250 }, - "secure" : 1, - "ext" : { - "tid" : "${json-unit.any-string}", - "bidder" : { - "accountId" : 2001, - "siteId" : 3001, - "zoneId" : 4001 - } + "pmp": { + "deals": [ + { + "id": "dealId" + } + ] + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001 + }, + "someExt": "someExt" } } ], "site" : { From f486ca5c23529af1785b33bbe7e6a7a82ee1e8ad Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:22:23 +0300 Subject: [PATCH 013/170] OwnAdx: Add new bidder (#2868) --- .../server/bidder/ownadx/OwnAdxBidder.java | 142 +++++++ .../ext/request/ownadx/ExtImpOwnAdx.java | 17 + .../bidder/OwnAdxBidderConfiguration.java | 41 +++ src/main/resources/bidder-config/ownadx.yaml | 20 + .../static/bidder-params/ownadx.json | 25 ++ .../bidder/ownadx/OwnAdxBidderTest.java | 345 ++++++++++++++++++ .../java/org/prebid/server/it/OwnAdxTest.java | 37 ++ .../ownadx/test-auction-ownadx-request.json | 25 ++ .../ownadx/test-auction-ownadx-response.json | 39 ++ .../ownadx/test-ownadx-bid-request.json | 58 +++ .../ownadx/test-ownadx-bid-response.json | 21 ++ .../server/it/test-application.properties | 2 + 12 files changed, 772 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java create mode 100644 src/main/resources/bidder-config/ownadx.yaml create mode 100644 src/main/resources/static/bidder-params/ownadx.json create mode 100644 src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/OwnAdxTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java new file mode 100644 index 00000000000..59258985635 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java @@ -0,0 +1,142 @@ +package org.prebid.server.bidder.ownadx; + +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; +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.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.ownadx.ExtImpOwnAdx; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class OwnAdxBidder implements Bidder { + + private static final TypeReference> OWN_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String X_OPEN_RTB_VERSION = "2.5"; + private static final String SEAT_ID_MACROS_ENDPOINT = "{{SeatID}}"; + private static final String SSP_ID_MACROS_ENDPOINT = "{{SspID}}"; + private static final String TOKEN_ID_MACROS_ENDPOINT = "{{TokenID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OwnAdxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + for (Imp imp : bidRequest.getImp()) { + try { + final ExtImpOwnAdx impOwnAdx = parseImpExt(imp); + httpRequests.add(createHttpRequest(bidRequest, impOwnAdx)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpOwnAdx parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), OWN_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } + } + + private HttpRequest createHttpRequest(BidRequest bidRequest, ExtImpOwnAdx extImpOwnAdx) { + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImpOwnAdx)) + .headers(makeHeaders()) + .body(mapper.encodeToBytes(bidRequest)) + .impIds(BidderUtil.impIds(bidRequest)) + .payload(bidRequest) + .build(); + } + + private String makeUrl(ExtImpOwnAdx extImpOwnAdx) { + final Optional ownAdx = Optional.ofNullable(extImpOwnAdx); + return endpointUrl + .replace(SEAT_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getSeatId).orElse(StringUtils.EMPTY)) + .replace(SSP_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getSspId).orElse(StringUtils.EMPTY)) + .replace(TOKEN_ID_MACROS_ENDPOINT, ownAdx.map(ExtImpOwnAdx::getTokenId).orElse(StringUtils.EMPTY)); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPEN_RTB_VERSION); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidMediaType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidMediaType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType " + bid.getMtype()); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java new file mode 100644 index 00000000000..6206f540b92 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ownadx/ExtImpOwnAdx.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.ownadx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOwnAdx { + + @JsonProperty("sspId") + String sspId; + + @JsonProperty("seatId") + String seatId; + + @JsonProperty("tokenId") + String tokenId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java new file mode 100644 index 00000000000..b34028b4221 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OwnAdxBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.ownadx.OwnAdxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/ownadx.yaml", factory = YamlPropertySourceFactory.class) +public class OwnAdxBidderConfiguration { + + private static final String BIDDER_NAME = "ownadx"; + + @Bean("ownAdxConfigurationProperties") + @ConfigurationProperties("adapters.ownadx") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps ownAdxBidderDeps(BidderConfigurationProperties ownAdxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(ownAdxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OwnAdxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/ownadx.yaml b/src/main/resources/bidder-config/ownadx.yaml new file mode 100644 index 00000000000..c836d847f98 --- /dev/null +++ b/src/main/resources/bidder-config/ownadx.yaml @@ -0,0 +1,20 @@ +adapters: + ownadx: + endpoint: "https://pbs.prebid-ownadx.com/bidder/bid/{{SeatID}}/{{SspID}}?token={{TokenID}}" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid-team@techbravo.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: ownadx + redirect: + url: https://sync.spoutroserve.com/user-sync?t=image&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&s3={{redirect_url}} + support-cors: false + uid-macro: '{USER_ID}' diff --git a/src/main/resources/static/bidder-params/ownadx.json b/src/main/resources/static/bidder-params/ownadx.json new file mode 100644 index 00000000000..fae8689bf55 --- /dev/null +++ b/src/main/resources/static/bidder-params/ownadx.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "OwnAdx Adapter Params", + "description": "A schema which validates params accepted by the OwnAdx adapter", + "type": "object", + "properties": { + "sspId": { + "type": "string", + "description": "Ssp ID" + }, + "seatId": { + "type": "string", + "description": "Seat ID" + }, + "tokenId": { + "type": "string", + "description": "Token ID" + } + }, + "required": [ + "sspId", + "seatId", + "tokenId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java b/src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java new file mode 100644 index 00000000000..ac520ff3883 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/ownadx/OwnAdxBidderTest.java @@ -0,0 +1,345 @@ +package org.prebid.server.bidder.ownadx; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ownadx.ExtImpOwnAdx; + +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; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +public class OwnAdxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.com/test/{{SeatID}}/{{SspID}}?token={{TokenID}}"; + + private final OwnAdxBidder target = new OwnAdxBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new OwnAdxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldPassImpIdToHttpRequestImpIdsAndCorrectlyPopulateBidRequest() { + // given + final String firstId = "234"; + final String secondId = "123"; + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of( + givenImp(impBuilder -> impBuilder.id(firstId)), + givenImp(impBuilder -> impBuilder.id(secondId)))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .flatExtracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(firstId, secondId, firstId, secondId); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .containsExactlyInAnyOrder(bidRequest, bidRequest); + } + + @Test + public void makeHttpRequestsShouldReturnOneValidAndInvalidInHttpRequest() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of( + givenImp(impBuilder -> impBuilder + .id("321") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))), + givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1); + assertThat(result.getErrors()) + .hasSize(1) + .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 321")); + } + + @Test + public void makeHttpRequestsShouldCorrectlyAddHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .flatExtracting(res -> res.getHeaders().entries()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple("Content-Type", "application/json;charset=utf-8"), + tuple("Accept", "application/json"), + tuple("x-openrtb-version", "2.5")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorsOfNotValidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .containsExactly(BidderError.badInput("Missing bidder ext in impression with id: 123")); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.banner(Banner.builder() + .format(singletonList(Format.builder().w(300).h(500).build())) + .build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.com/test/seatId/sspId?token=token"); + } + + @Test + public void makeHttpRequestsShouldNotReplaceMacrosWhenInExtOwnAdxAllFieldAreNull() { + // given + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpOwnAdx.of(null, null, null))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.com/test//?token="); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfMtypeIsOne() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(1)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfMtypeIsTwo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(2)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidIfMtypeIsThree() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + 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()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfMtypeIsFour() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(4)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenTypeContainUnknownValue() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123").mtype(10)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .satisfies(error -> { + assertThat(error).extracting(BidderError::getType) + .containsExactly(BidderError.Type.bad_server_response); + assertThat(error).extracting(BidderError::getMessage) + .containsExactly("Unable to fetch mediaType 10"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenTypeIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.id("123").mtype(null)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()) + .hasSize(1) + .satisfies(error -> { + assertThat(error).extracting(BidderError::getType) + .containsExactly(BidderError.Type.bad_server_response); + assertThat(error).extracting(BidderError::getMessage) + .containsExactly("Missing MType for bid: 123"); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(UnaryOperator.identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOwnAdx.of("sspId", "seatId", "token"))))) + .build(); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/OwnAdxTest.java b/src/test/java/org/prebid/server/it/OwnAdxTest.java new file mode 100644 index 00000000000..1ab4ddb5d71 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OwnAdxTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +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; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class OwnAdxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOwnAdx() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/ownadx-exchange/bid/testSeatId/testSspId")) + .withQueryParam("token", equalTo("testTokenId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/ownadx/test-ownadx-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/ownadx/test-ownadx-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/ownadx/test-auction-ownadx-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/ownadx/test-auction-ownadx-response.json", response, singletonList("ownadx")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json new file mode 100644 index 00000000000..cf500a4f09c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "ownadx": { + "tokenId": "testTokenId", + "seatId": "testSeatId", + "sspId": "testSspId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json new file mode 100644 index 00000000000..38e6c451540 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "ownadx", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "ownadx": "{{ ownadx.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json new file mode 100644 index 00000000000..191c8b5661a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "tokenId": "testTokenId", + "seatId" : "testSeatId", + "sspId" : "testSspId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json new file mode 100644 index 00000000000..9eef9d710d7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-ownadx-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype" : 1, + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} 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 9e313b9d896..53834b73a99 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -320,6 +320,8 @@ adapters.orbidder.enabled=true adapters.orbidder.endpoint=http://localhost:8090/orbidder-exchange adapters.outbrain.enabled=true adapters.outbrain.endpoint=http://localhost:8090/outbrain-exchange +adapters.ownadx.enabled=true +adapters.ownadx.endpoint=http://localhost:8090/ownadx-exchange/bid/{{SeatID}}/{{SspID}}?token={{TokenID}} adapters.pangle.enabled=true adapters.pangle.endpoint=http://localhost:8090/pangle-exchange adapters.pgamssp.enabled=true From 344cb98dbba1bf346db029fbc31267d0dcd2ca13 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:25:34 +0200 Subject: [PATCH 014/170] TheTradeDesk: Add Bidder (#3370) --- .../thetradedesk/TheTradeDeskBidder.java | 204 ++++++++ .../thetradedesk/ExtImpTheTradeDesk.java | 11 + .../bidder/TheTradeDeskConfiguration.java | 62 +++ .../resources/bidder-config/thetradedesk.yaml | 15 + .../static/bidder-params/thetradedesk.json | 15 + .../thetradedesk/TheTradeDeskBidderTest.java | 459 ++++++++++++++++++ .../prebid/server/it/TheTradeDeskTest.java | 40 ++ .../test-auction-thetradedesk-request.json | 23 + .../test-auction-thetradedesk-response.json | 36 ++ .../test-thetradedesk-bid-request.json | 57 +++ .../test-thetradedesk-bid-response.json | 19 + .../server/it/test-application.properties | 3 + 12 files changed, 944 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java create mode 100644 src/main/resources/bidder-config/thetradedesk.yaml create mode 100644 src/main/resources/static/bidder-params/thetradedesk.json create mode 100644 src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/TheTradeDeskTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java new file mode 100644 index 00000000000..c9a8366f9ac --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -0,0 +1,204 @@ +package org.prebid.server.bidder.thetradedesk; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +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; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.thetradedesk.ExtImpTheTradeDesk; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +public class TheTradeDeskBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PREBID_INTEGRATION_TYPE_HEADER = "x-integration-type"; + private static final String PREBID_INTEGRATION_TYPE = "1"; + private static final MultiMap HEADERS = HttpUtil.headers() + .add(PREBID_INTEGRATION_TYPE_HEADER, PREBID_INTEGRATION_TYPE); + + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$"); + + private final String endpointUrl; + private final String supplyId; + private final JacksonMapper mapper; + + public TheTradeDeskBidder(String endpointUrl, JacksonMapper mapper, String supplyId) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.supplyId = validateSupplyId(supplyId); + this.mapper = Objects.requireNonNull(mapper); + } + + private static String validateSupplyId(String supplyId) { + if (StringUtils.isBlank(supplyId) || SUPPLY_ID_PATTERN.matcher(supplyId).matches()) { + return supplyId; + } + + throw new IllegalArgumentException("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + + String publisherId = null; + for (Imp imp : request.getImp()) { + try { + final ExtImpTheTradeDesk extImp = parseImpExt(imp); + publisherId = publisherId == null + ? StringUtils.isNotBlank(extImp.getPublisherId()) + ? extImp.getPublisherId() + : publisherId + : publisherId; + + modifiedImps.add(modifyImp(imp)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + HEADERS, + resolveEndpoint(), + mapper); + + return Result.withValue(httpRequest); + } + + private ExtImpTheTradeDesk parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage(), e); + } + } + + private static Imp modifyImp(Imp imp) { + final Banner banner = imp.getBanner(); + + if (banner != null && CollectionUtils.isNotEmpty(banner.getFormat())) { + final Format format = banner.getFormat().getFirst(); + return imp.toBuilder() + .banner(banner.toBuilder().w(format.getW()).h(format.getH()).build()) + .build(); + } + + return imp; + } + + private static BidRequest modifyRequest(BidRequest request, List modifiedImps, String publisherId) { + return request.toBuilder() + .imp(modifiedImps) + .site(modifySite(request, publisherId)) + .app(modifyApp(request, publisherId)) + .build(); + } + + private static Site modifySite(BidRequest request, String publisherId) { + final Site site = request.getSite(); + if (site == null) { + return null; + } + + return site.toBuilder() + .publisher(modifyPublisher(site.getPublisher(), publisherId)) + .build(); + } + + private static Publisher modifyPublisher(Publisher publisher, String publisherId) { + if (publisher == null) { + return Publisher.builder().id(publisherId).build(); + } + + return publisher.toBuilder() + .id(StringUtils.isNotBlank(publisherId) ? publisherId : publisher.getId()) + .build(); + } + + private static App modifyApp(BidRequest request, String publisherId) { + final Site site = request.getSite(); + final App app = request.getApp(); + + if (site != null) { + return app; + } + + if (app == null) { + return null; + } + + return app.toBuilder() + .publisher(modifyPublisher(app.getPublisher(), publisherId)) + .build(); + } + + private String resolveEndpoint() { + return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(supplyId))); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype())); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java new file mode 100644 index 00000000000..ee3b9cc7d83 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.thetradedesk; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTheTradeDesk { + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java new file mode 100644 index 00000000000..899c29d2293 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TheTradeDeskConfiguration.java @@ -0,0 +1,62 @@ +package org.prebid.server.spring.config.bidder; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.thetradedesk.TheTradeDeskBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/thetradedesk.yaml", factory = YamlPropertySourceFactory.class) +public class TheTradeDeskConfiguration { + + private static final String BIDDER_NAME = "thetradedesk"; + + @Bean("thetradedeskConfigurationProperties") + @ConfigurationProperties("adapters.thetradedesk") + TheTradeDeskConfigurationProperties configurationProperties() { + return new TheTradeDeskConfigurationProperties(); + } + + @Bean + BidderDeps theTradeDeskBidderDeps(TheTradeDeskConfigurationProperties theTradeDeskConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(theTradeDeskConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TheTradeDeskBidder( + config.getEndpoint(), + mapper, + config.getExtraInfo().getSupplyId()) + ).assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class TheTradeDeskConfigurationProperties extends BidderConfigurationProperties { + + private ExtraInfo extraInfo = new ExtraInfo(); + } + + @Data + @NoArgsConstructor + private static class ExtraInfo { + + String supplyId; + } +} diff --git a/src/main/resources/bidder-config/thetradedesk.yaml b/src/main/resources/bidder-config/thetradedesk.yaml new file mode 100644 index 00000000000..b71a0e38cc2 --- /dev/null +++ b/src/main/resources/bidder-config/thetradedesk.yaml @@ -0,0 +1,15 @@ +adapters: + thetradedesk: + endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}} + meta-info: + maintainer-email: Prebid-Maintainers@thetradedesk.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 21 diff --git a/src/main/resources/static/bidder-params/thetradedesk.json b/src/main/resources/static/bidder-params/thetradedesk.json new file mode 100644 index 00000000000..d0b305a5a1e --- /dev/null +++ b/src/main/resources/static/bidder-params/thetradedesk.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "The Trade Desk Adapter Params", + "description": "A schema which validates params accepted by the The Trade Desk adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "An ID which identifies the publisher" + } + }, + "required": [ + "publisherId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java new file mode 100644 index 00000000000..4fee1810ad3 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java @@ -0,0 +1,459 @@ +package org.prebid.server.bidder.thetradedesk; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class TheTradeDeskBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/{{SupplyId}}"; + + private final TheTradeDeskBidder target = new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supplyid"); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder("invalid_url", jacksonMapper, "supplyid")); + } + + @Test + public void creationShouldFailWhenSupplyIdHasNumbers() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supplyid123")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void creationShouldFailWhenSupplyIdHasSpecificCharacters() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supply_id")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void creationShouldFailWhenSupplyIdHasCapitalLetters() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supplyId")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void creationShouldFailWhenSupplyIdHasWhitespaces() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new TheTradeDeskBidder(ENDPOINT_URL, jacksonMapper, "supply id")) + .withMessage("SupplyId must be a simple string provided by TheTradeDesk"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get("x-integration-type")) + .isEqualTo("1")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/supplyid"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + imp -> imp.id("givenImp1"), + imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Set.of("givenImp1", "givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Cannot deserialize value"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnSiteWithExtImpPublisherWhenSiteAndAppArePresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(Publisher.builder().id("sitePublisher").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(Publisher.builder().id("newPublisher").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnSiteWithExtImpPublisherWhenSiteWithoutPublisherAndAppArePresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(null).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .site(Site.builder().publisher(Publisher.builder().id("newPublisher").build()).build()) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnAppWithExtImpPublisherWhenSiteIsAbsentAndAppIsPresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(null) + .app(App.builder().publisher(Publisher.builder().id("appPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .app(App.builder().publisher(Publisher.builder().id("newPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnAppWithExtImpPublisherWhenAppWithoutPublisherArePresent() { + final BidRequest bidRequest = givenBidRequest( + request -> request + .site(null) + .app(App.builder().publisher(null).build()), + imp -> imp.ext(impExt("newPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request + .app(App.builder().publisher(Publisher.builder().id("newPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnAppWithPublisherOfTheFirsrExtImp() { + final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().build()), + imp -> imp.ext(impExt("newPublisher")), + imp -> imp.ext(impExt("ignoredPublisher"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + request -> request.app(App.builder().publisher(Publisher.builder().id("newPublisher").build()).build()), + imp -> imp.ext(impExt("newPublisher")), + imp -> imp.ext(impExt("ignoredPublisher"))); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeHttpRequestsShouldReturnBannerImpWithTheFirstFormat() { + final BidRequest bidRequest = givenBidRequest( + identity(), + imp -> imp.banner(Banner.builder() + .h(1) + .w(2) + .format(List.of( + Format.builder().h(11).w(22).build(), + Format.builder().h(111).w(222).build())) + .build()), + imp -> imp.banner(null).video(Video.builder().build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final BidRequest expectedRequest = givenBidRequest( + identity(), + imp -> imp.banner(Banner.builder() + .h(11) + .w(22) + .format(List.of( + Format.builder().h(11).w(22).build(), + Format.builder().h(111).w(222).build())) + .build()), + imp -> imp.video(Video.builder().build())); + + assertThat(result.getValue()).hasSize(1).first() + .satisfies(request -> assertThat(request.getBody()) + .isEqualTo(jacksonMapper.encodeToBytes(expectedRequest))) + .satisfies(request -> assertThat(request.getPayload()) + .isEqualTo(expectedRequest)); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badServerResponse("unsupported mtype: null")); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator... impCustomizers) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(TheTradeDeskBidderTest::givenImp).toList())) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("123").ext(impExt("publisherId"))).build(); + } + + private static ObjectNode impExt(String publisherId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId))); + } + +} diff --git a/src/test/java/org/prebid/server/it/TheTradeDeskTest.java b/src/test/java/org/prebid/server/it/TheTradeDeskTest.java new file mode 100644 index 00000000000..f2ea877239f --- /dev/null +++ b/src/test/java/org/prebid/server/it/TheTradeDeskTest.java @@ -0,0 +1,40 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; + +public class TheTradeDeskTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheTradeDesk() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/thetradedesk-exchange/somesupplyid")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/thetradedesk/test-thetradedesk-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/thetradedesk/test-thetradedesk-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/thetradedesk/test-auction-thetradedesk-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals( + "openrtb2/thetradedesk/test-auction-thetradedesk-response.json", + response, + List.of("thetradedesk")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json new file mode 100644 index 00000000000..2f1bdf270d1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "thetradedesk": { + "publisherId": "publisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json new file mode 100644 index 00000000000..9675846a5ce --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "thetradedesk", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "thetradedesk": "{{ thetradedesk.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json new file mode 100644 index 00000000000..fde2176c406 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-request.json @@ -0,0 +1,57 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "publisherId": "publisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "publisherId", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-response.json new file mode 100644 index 00000000000..47d4f8718ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-thetradedesk-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} 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 53834b73a99..9c32c0638d0 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -425,6 +425,9 @@ adapters.telaria.enabled=true adapters.telaria.endpoint=http://localhost:8090/telaria-exchange/ adapters.theadx.enabled=true adapters.theadx.endpoint=http://localhost:8090/theadx-exchange +adapters.thetradedesk.enabled=true +adapters.thetradedesk.endpoint=http://localhost:8090/thetradedesk-exchange/{{SupplyId}} +adapters.thetradedesk.extra-info.supply-id=somesupplyid adapters.triplelift.enabled=true adapters.triplelift.endpoint=http://localhost:8090/triplelift-exchange adapters.tripleliftnative.enabled=true From e056b48c87b46b844b4ee939fbe9a79bb8a12d6e Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Tue, 13 Aug 2024 16:27:03 +0300 Subject: [PATCH 015/170] Rubicon: Updated xapi integration (#3377) --- .../server/bidder/rubicon/RubiconBidder.java | 18 +++- .../config/bidder/RubiconConfiguration.java | 4 + .../bidder/rubicon/RubiconBidderTest.java | 90 ++++++++++++++----- .../org/prebid/server/it/MagniteTest.java | 8 +- .../org/prebid/server/it/RubiconTest.java | 8 +- .../org/prebid/server/it/hooks/HooksTest.java | 14 ++- .../reject/test-rubicon-bid-request-1.json | 5 +- .../test-rubicon-bid-request-1.json | 5 +- .../magnite/test-magnite-bid-request.json | 5 +- .../rubicon/test-rubicon-bid-request.json | 5 +- 10 files changed, 131 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 931de1bf1fa..e6f49dd6f43 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -110,6 +110,7 @@ import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ListUtil; import org.prebid.server.util.ObjectUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.net.URISyntaxException; @@ -154,6 +155,9 @@ public class RubiconBidder implements Bidder { private static final String DFP_ADUNIT_CODE_FIELD = "dfp_ad_unit_code"; private static final String STYPE_FIELD = "stype"; private static final String PREBID_EXT = "prebid"; + private static final String PBS_LOGIN = "pbs_login"; + private static final String PBS_VERSION = "pbs_version"; + private static final String PBS_URL = "pbs_url"; private static final String PPUID_STYPE = "ppuid"; private static final String SHA256EMAIL_STYPE = "sha256email"; @@ -180,17 +184,21 @@ public class RubiconBidder implements Bidder { private final String bidderName; private final String endpointUrl; + private final String externalUrl; + private final String xapiUsername; private final Set supportedVendors; private final boolean generateBidId; private final boolean useVideoSizeLogic; private final CurrencyConversionService currencyConversionService; private final PriceFloorResolver floorResolver; + private final PrebidVersionProvider versionProvider; private final JacksonMapper mapper; private final MultiMap headers; public RubiconBidder(String bidderName, String endpoint, + String externalUrl, String xapiUsername, String xapiPassword, List supportedVendors, @@ -198,15 +206,19 @@ public RubiconBidder(String bidderName, boolean useVideoSizeLogic, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, JacksonMapper mapper) { this.bidderName = Objects.requireNonNull(bidderName); this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.externalUrl = HttpUtil.validateUrl(Objects.requireNonNull(externalUrl)); + this.xapiUsername = Objects.requireNonNull(xapiUsername); this.supportedVendors = Set.copyOf(Objects.requireNonNull(supportedVendors)); this.generateBidId = generateBidId; this.useVideoSizeLogic = useVideoSizeLogic; this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.floorResolver = Objects.requireNonNull(floorResolver); + this.versionProvider = Objects.requireNonNull(versionProvider); this.mapper = Objects.requireNonNull(mapper); headers = headers(Objects.requireNonNull(xapiUsername), Objects.requireNonNull(xapiPassword)); @@ -704,7 +716,11 @@ private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App mergeFirstPartyDataFromApp(app, result); mergeFirstPartyDataFromImp(imp, rubiconImpExt, result); - return !result.isEmpty() ? result : null; + result.put(PBS_LOGIN, xapiUsername); + result.put(PBS_VERSION, versionProvider.getNameVersionRecord()); + result.put(PBS_URL, externalUrl); + + return result; } private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult, diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java index 9580f229f1f..d78f5324c01 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java @@ -12,6 +12,7 @@ import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.version.PrebidVersionProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -40,6 +41,7 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration @NotBlank @Value("${external-url}") String externalUrl, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) @@ -49,6 +51,7 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration new RubiconBidder( BIDDER_NAME, config.getEndpoint(), + externalUrl, config.getXapi().getUsername(), config.getXapi().getPassword(), config.getMetaInfo().getSupportedVendors(), @@ -56,6 +59,7 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration config.getUseVideoSizeIdLogic(), currencyConversionService, floorResolver, + versionProvider, mapper)) .assemble(); } diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index 98f3d12347a..d19933818ee 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -108,6 +108,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.io.IOException; import java.math.BigDecimal; @@ -147,8 +148,10 @@ public class RubiconBidderTest extends VertxTest { private static final String BIDDER_NAME = "bidderName"; private static final String ENDPOINT_URL = "http://rubiconproject.com/exchange.json?tk_xint=prebid"; + private static final String EXTERNAL_URL = "http://localhost:8080"; private static final String USERNAME = "username"; private static final String PASSWORD = "password"; + private static final String PBS_VERSION = "pbs_version"; private static final List SUPPORTED_VENDORS = Arrays.asList("activeview", "comscore", "doubleverify", "integralads", "moat", "sizmek", "whiteops"); @@ -158,12 +161,17 @@ public class RubiconBidderTest extends VertxTest { @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; + @Mock(strictness = LENIENT) + private PrebidVersionProvider versionProvider; + private RubiconBidder target; @BeforeEach public void setUp() { - target = new RubiconBidder(BIDDER_NAME, + target = new RubiconBidder( + BIDDER_NAME, ENDPOINT_URL, + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, @@ -171,7 +179,10 @@ public void setUp() { true, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper); + + given(versionProvider.getNameVersionRecord()).willReturn("pbs_version"); } @Test @@ -179,6 +190,7 @@ public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy( () -> new RubiconBidder(BIDDER_NAME, "invalid_url", + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, @@ -186,6 +198,7 @@ public void creationShouldFailOnInvalidEndpointUrl() { true, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper)); } @@ -623,17 +636,17 @@ public void makeHttpRequestsShouldFillImpExt() { final Result>> result = target.makeHttpRequests(bidRequest); // then + final ObjectNode expectedTarget = givenImpExtRpTarget().setAll( + (ObjectNode) mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech")))); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1).doesNotContainNull() .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).doesNotContainNull() .extracting(Imp::getExt).doesNotContainNull() .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)) - .containsOnly(RubiconImpExt.builder() - .rp(RubiconImpExtRp.of(4001, - mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))), - RubiconImpExtRpTrack.of("", ""), - null)) + .containsExactly(RubiconImpExt.builder() + .rp(RubiconImpExtRp.of(4001, expectedTarget, RubiconImpExtRpTrack.of("", ""), null)) .skadn(givenSkadn) .maxbids(1) .build()); @@ -832,6 +845,7 @@ public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsI target = new RubiconBidder( BIDDER_NAME, ENDPOINT_URL, + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, @@ -839,6 +853,7 @@ public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsI false, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper); final BidRequest bidRequest = givenBidRequest( builder -> builder.instl(1).video(Video.builder().placement(1).build()), @@ -863,6 +878,7 @@ public void shouldSetSizeFromBidderParamsWhenVideoSizeProcessingLogicIsDisabled( target = new RubiconBidder( BIDDER_NAME, ENDPOINT_URL, + EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, @@ -870,6 +886,7 @@ public void shouldSetSizeFromBidderParamsWhenVideoSizeProcessingLogicIsDisabled( false, currencyConversionService, priceFloorResolver, + versionProvider, jacksonMapper); final BidRequest bidRequest = givenBidRequest( builder -> builder.instl(1).video(Video.builder().placement(1).build()), @@ -2493,11 +2510,14 @@ public void makeHttpRequestsShouldCreateRequestPerImp() { final Result>> result = target.makeHttpRequests(bidRequest); // then + final RubiconImpExtRp expectedImpExtRp = RubiconImpExtRp.of( + null, givenImpExtRpTarget(), RubiconImpExtRpTrack.of("", ""), null); + final BidRequest expectedBidRequest1 = BidRequest.builder() .imp(singletonList(Imp.builder() .video(Video.builder().build()) .ext(mapper.valueToTree(RubiconImpExt.builder() - .rp(RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", ""), null)) + .rp(expectedImpExtRp) .maxbids(1) .build())) .build())) @@ -2508,7 +2528,7 @@ public void makeHttpRequestsShouldCreateRequestPerImp() { .video(Video.builder().build()) .ext(mapper.valueToTree( RubiconImpExt.builder() - .rp(RubiconImpExtRp.of(null, null, RubiconImpExtRpTrack.of("", ""), null)) + .rp(expectedImpExtRp) .maxbids(1) .build())) .build())) @@ -2544,8 +2564,7 @@ public void makeHttpRequestsShouldCopyAndModifyDataFieldsToRubiconImpExtRpTarget .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode() - .set("property2", mapper.createArrayNode().add("value2"))); + .containsExactly(givenImpExtRpTarget().set("property2", mapper.createArrayNode().add("value2"))); } @Test @@ -2592,7 +2611,7 @@ public void makeHttpRequestsShouldCopySiteExtDataFieldsToRubiconImpExtRpTarget() .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode().set("property", mapper.createArrayNode().add("value"))); + .containsExactly(givenImpExtRpTarget().set("property", mapper.createArrayNode().add("value"))); } @Test @@ -2618,7 +2637,27 @@ public void makeHttpRequestsShouldCopyAppExtDataFieldsToRubiconImpExtRpTarget() .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode().set("property", mapper.createArrayNode().add("value"))); + .containsOnly(givenImpExtRpTarget().set("property", mapper.createArrayNode().add("value"))); + } + + @Test + public void makeHttpRequestsShouldSetXapiFieldsToRubiconImpExtRpTarget() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.video(Video.builder().build())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) + .extracting(RubiconImpExt::getRp) + .extracting(RubiconImpExtRp::getTarget) + .containsExactly(givenImpExtRpTarget()); } @Test @@ -2737,6 +2776,9 @@ public void makeHttpRequestsShouldCopyDataSearchToRubiconImpExtRpTargetSearch() final Result>> result = target.makeHttpRequests(bidRequest); // then + final ObjectNode expectedTarget = givenImpExtRpTarget() + .set("search", mapper.createArrayNode().add("imp ext data search")); + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) @@ -2745,7 +2787,7 @@ public void makeHttpRequestsShouldCopyDataSearchToRubiconImpExtRpTargetSearch() .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.readTree("{\"search\":[\"imp ext data search\"]}")); + .containsExactly(expectedTarget); } @Test @@ -2794,8 +2836,7 @@ public void makeHttpRequestsShouldMergeSiteAttributesAndCopyToRubiconImpExtRpTar .extracting(objectNode -> mapper.convertValue(objectNode, RubiconImpExt.class)) .extracting(RubiconImpExt::getRp) .extracting(RubiconImpExtRp::getTarget) - .containsOnly(mapper.createObjectNode() - .set("page", mapper.createArrayNode().add("site page"))); + .containsExactly(givenImpExtRpTarget().set("page", mapper.createArrayNode().add("site page"))); } @Test @@ -3002,8 +3043,8 @@ public void makeHttpRequestsShouldReturnOnlyLineItemRequestsWithExpectedFieldsWh .flatExtracting(BidRequest::getImp) .extracting(imp -> mapper.treeToValue(imp.getExt(), RubiconImpExt.class).getRp().getTarget()) .containsOnly( - mapper.readTree("{\"line_item\":\"123\"}"), - mapper.readTree("{\"line_item\":\"234\"}")); + givenImpExtRpTarget().put("line_item", "123"), + givenImpExtRpTarget().put("line_item", "234")); } @Test @@ -3719,8 +3760,8 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessingException { // given target = new RubiconBidder( - BIDDER_NAME, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, - currencyConversionService, priceFloorResolver, jacksonMapper); + BIDDER_NAME, ENDPOINT_URL, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, + currencyConversionService, priceFloorResolver, versionProvider, jacksonMapper); final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), mapper.writeValueAsString(RubiconBidResponse.builder() @@ -3745,8 +3786,8 @@ public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessi public void makeBidsShouldReturnBidWithCurrencyFromBidResponse() throws JsonProcessingException { // given target = new RubiconBidder( - BIDDER_NAME, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, - currencyConversionService, priceFloorResolver, jacksonMapper); + BIDDER_NAME, ENDPOINT_URL, EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, + currencyConversionService, priceFloorResolver, versionProvider, jacksonMapper); final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), mapper.writeValueAsString(RubiconBidResponse.builder() @@ -3942,6 +3983,13 @@ private static Data givenTestDataWithSegmentEntries(Integer segtax) { .build(); } + private static ObjectNode givenImpExtRpTarget() { + return mapper.createObjectNode() + .put("pbs_login", USERNAME) + .put("pbs_version", PBS_VERSION) + .put("pbs_url", EXTERNAL_URL); + } + @AllArgsConstructor(staticName = "of") @Value private static class Inventory { diff --git a/src/test/java/org/prebid/server/it/MagniteTest.java b/src/test/java/org/prebid/server/it/MagniteTest.java index 6623ca7d9a9..c02387d55dc 100644 --- a/src/test/java/org/prebid/server/it/MagniteTest.java +++ b/src/test/java/org/prebid/server/it/MagniteTest.java @@ -4,6 +4,8 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.prebid.server.model.Endpoint; +import org.prebid.server.version.PrebidVersionProvider; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -15,11 +17,15 @@ public class MagniteTest extends IntegrationTest { + @Autowired + private PrebidVersionProvider versionProvider; + @Test public void testOpenrtb2AuctionCoreFunctionality() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/magnite-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/magnite/test-magnite-bid-request.json"))) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/magnite/test-magnite-bid-request.json", versionProvider))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/magnite/test-magnite-bid-response.json")))); // when diff --git a/src/test/java/org/prebid/server/it/RubiconTest.java b/src/test/java/org/prebid/server/it/RubiconTest.java index 85f521abb80..d7799bc50eb 100644 --- a/src/test/java/org/prebid/server/it/RubiconTest.java +++ b/src/test/java/org/prebid/server/it/RubiconTest.java @@ -4,6 +4,8 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.prebid.server.model.Endpoint; +import org.prebid.server.version.PrebidVersionProvider; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -15,11 +17,15 @@ public class RubiconTest extends IntegrationTest { + @Autowired + PrebidVersionProvider versionProvider; + @Test public void testOpenrtb2AuctionCoreFunctionality() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/rubicon/test-rubicon-bid-request.json"))) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon/test-rubicon-bid-request.json", versionProvider))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/rubicon/test-rubicon-bid-response.json")))); // when diff --git a/src/test/java/org/prebid/server/it/hooks/HooksTest.java b/src/test/java/org/prebid/server/it/hooks/HooksTest.java index 21664060409..3943338630c 100644 --- a/src/test/java/org/prebid/server/it/hooks/HooksTest.java +++ b/src/test/java/org/prebid/server/it/hooks/HooksTest.java @@ -4,8 +4,10 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.prebid.server.it.IntegrationTest; +import org.prebid.server.version.PrebidVersionProvider; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; +import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -22,11 +24,15 @@ public class HooksTest extends IntegrationTest { private static final String RUBICON = "rubicon"; + @Autowired + private PrebidVersionProvider versionProvider; + @Test public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("hooks/sample-module/test-rubicon-bid-request-1.json"))) + .withRequestBody(equalToJson( + jsonFrom("hooks/sample-module/test-rubicon-bid-request-1.json", versionProvider))) .willReturn(aResponse().withBody(jsonFrom("hooks/sample-module/test-rubicon-bid-response-1.json")))); // when @@ -113,7 +119,8 @@ public void openrtb2AuctionShouldRejectRubiconBidderByRawBidderResponseHook() th JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); WIRE_MOCK_RULE.verify(1, postRequestedFor(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("hooks/reject/test-rubicon-bid-request-1.json")))); + .withRequestBody(equalToJson( + jsonFrom("hooks/reject/test-rubicon-bid-request-1.json", versionProvider)))); } @Test @@ -139,6 +146,7 @@ public void openrtb2AuctionShouldRejectRubiconBidderByProcessedBidderResponseHoo JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT); WIRE_MOCK_RULE.verify(1, postRequestedFor(urlPathEqualTo("/rubicon-exchange")) - .withRequestBody(equalToJson(jsonFrom("hooks/reject/test-rubicon-bid-request-1.json")))); + .withRequestBody(equalToJson( + jsonFrom("hooks/reject/test-rubicon-bid-request-1.json", versionProvider)))); } } diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json index 5637a0f0026..0daecc354d4 100644 --- a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json @@ -27,7 +27,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json index 89d2ac57bcb..14d752bc96d 100644 --- a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json +++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json @@ -28,7 +28,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json index e1f24125c13..08104541960 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json @@ -17,7 +17,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json index e1f24125c13..08104541960 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json @@ -17,7 +17,10 @@ "target": { "page": [ "http://www.example.com" - ] + ], + "pbs_version": "{{ pbs.java.version }}", + "pbs_login": "rubicon_user", + "pbs_url": "http://localhost:8080" }, "track": { "mint": "", From 4c8363ceb03af589c528b24b862d8f831d12a884 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:40:57 -0400 Subject: [PATCH 016/170] Prebid Server prepare release 3.9.0 --- extra/bundle/pom.xml | 5 ++--- extra/modules/confiant-ad-quality/pom.xml | 5 ++--- extra/modules/fiftyone-devicedetection/pom.xml | 5 ++--- extra/modules/ortb2-blocking/pom.xml | 5 ++--- extra/modules/pb-richmedia-filter/pom.xml | 5 ++--- extra/modules/pom.xml | 5 ++--- extra/pom.xml | 7 +++---- pom.xml | 5 ++--- 8 files changed, 17 insertions(+), 25 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 069e8c64f69..09f31a9de94 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.9.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 77602a9c00b..187ef114810 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.9.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 6116a2be58c..17b0e5870ee 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.9.0 fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index f6d298e0947..7491df5b7f8 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.9.0 ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index d177812c588..173b567e54f 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.9.0-SNAPSHOT + 3.9.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 0240ae00a38..9f1c9ad3629 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.9.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index f5bd454a313..3a84a05b57d 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -1,18 +1,17 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.9.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.9.0 diff --git a/pom.xml b/pom.xml index 029d2babfad..f1c0ae4b808 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.prebid prebid-server-aggregator - 3.9.0-SNAPSHOT + 3.9.0 extra/pom.xml From de7a71d43ac153869bc1f527a4980c466636ffd1 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:44:07 -0400 Subject: [PATCH 017/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 09f31a9de94..29e21758220 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.9.0 + 3.10.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 187ef114810..b4eca575054 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.9.0 + 3.10.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 17b0e5870ee..65a7742f291 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.9.0 + 3.10.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 7491df5b7f8..32a47778840 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.9.0 + 3.10.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 173b567e54f..658919626ce 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.9.0 + 3.10.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 9f1c9ad3629..37a31edc344 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.9.0 + 3.10.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 3a84a05b57d..64bc727fd00 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.9.0 + 3.10.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.9.0 + HEAD diff --git a/pom.xml b/pom.xml index f1c0ae4b808..a097bde4b7b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.9.0 + 3.10.0-SNAPSHOT extra/pom.xml From 53d615af148f20375474eb86d58aaa0fb70c3d8f Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:22:12 +0300 Subject: [PATCH 018/170] Rubicon: Fix currency conversion bug (#3380) --- .../server/bidder/rubicon/RubiconBidder.java | 2 +- .../bidder/rubicon/RubiconBidderTest.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index e6f49dd6f43..675d5dd4bd5 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -618,7 +618,7 @@ private BigDecimal convertToXAPICurrency(BigDecimal value, private static BigDecimal resolveBidFloorPrice(Imp imp) { final BigDecimal bidFloor = imp.getBidfloor(); - return BidderUtil.isValidPrice(bidFloor) ? bidFloor : null; + return bidFloor != null && bidFloor.compareTo(BigDecimal.ZERO) >= 0 ? bidFloor : null; } private static String resolveBidFloorCurrency(Imp imp, BidRequest bidRequest, List errors) { diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index d19933818ee..b7c2a053298 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -789,6 +789,31 @@ public void makeHttpRequestsShouldResolveImpBidFloorCurrencyIfNotUSDAndCallCurre .containsOnly(tuple(BigDecimal.TEN, "USD")); } + @Test + public void makeHttpRequestsShouldResolveImpBidFloorCurrencyIfNotUSDAndBidFloorIsZero() { + // given + final BidRequest bidRequest = givenBidRequest( + builder -> builder + .banner(Banner.builder().format(singletonList(Format.builder().w(300).h(250).build())).build()) + .bidfloor(BigDecimal.ZERO).bidfloorcur("EUR"), + identity()); + + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.ZERO); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + verify(currencyConversionService).convertCurrency(eq(BigDecimal.ZERO), any(), eq("EUR"), eq("USD")); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp).doesNotContainNull() + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ZERO, "USD")); + } + @Test public void makeHttpRequestsShouldNotSetBidFloorCurrencyToUSDIfNull() { // given From e034cae60a845af9e4315ba1658ba7c30ee1a475 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:00:56 +0200 Subject: [PATCH 019/170] LimeLightDigital: Filmzie Alias (#3384) --- .../bidder-config/limelightDigital.yaml | 2 + .../org/prebid/server/it/FilmzieTest.java | 35 ++++++++++++++++ .../filmzie/test-auction-filmzie-request.json | 24 +++++++++++ .../test-auction-filmzie-response.json | 33 +++++++++++++++ .../filmzie/test-filmzie-bid-request.json | 40 +++++++++++++++++++ .../filmzie/test-filmzie-bid-response.json | 15 +++++++ .../server/it/test-application.properties | 2 + 7 files changed, 151 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/FilmzieTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json diff --git a/src/main/resources/bidder-config/limelightDigital.yaml b/src/main/resources/bidder-config/limelightDigital.yaml index ab2336287e0..f13e008afd0 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -2,6 +2,8 @@ adapters: limelightDigital: endpoint: http://ads-pbs.ortb.net/openrtb/{{PublisherID}}?host={{Host}} aliases: + filmzie: + enabled: false iionads: enabled: false endpoint: http://ads-pbs.iionads.com/openrtb/{{PublisherID}}?host={{Host}} diff --git a/src/test/java/org/prebid/server/it/FilmzieTest.java b/src/test/java/org/prebid/server/it/FilmzieTest.java new file mode 100644 index 00000000000..fefbffb6e74 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FilmzieTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class FilmzieTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheFilmzieBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/filmzie-exchange/test.host/123456")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/filmzie/test-filmzie-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/filmzie/test-filmzie-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/filmzie/test-auction-filmzie-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/filmzie/test-auction-filmzie-response.json", response, + singletonList("filmzie")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json new file mode 100644 index 00000000000..00822b5da93 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "filmzie": { + "host": "test.host", + "publisherId": "123456" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json new file mode 100644 index 00000000000..4a1aac1a8c5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json @@ -0,0 +1,33 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "filmzie", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "filmzie": "{{ filmzie.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json new file mode 100644 index 00000000000..8e58e53ba4b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-request.json @@ -0,0 +1,40 @@ +{ + "id": "request_id-imp_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json new file mode 100644 index 00000000000..04d26e04318 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-filmzie-bid-response.json @@ -0,0 +1,15 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} 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 9c32c0638d0..be7834af223 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -269,6 +269,8 @@ adapters.limelightDigital.aliases.xtrmqb.enabled=true adapters.limelightDigital.aliases.xtrmqb.endpoint=http://localhost:8090/xtrmqb-exchange/{{Host}}/{{PublisherID}} adapters.limelightDigital.aliases.embimedia.enabled=true adapters.limelightDigital.aliases.embimedia.endpoint=http://localhost:8090/embimedia-exchange/{{Host}}/{{PublisherID}} +adapters.limelightDigital.aliases.filmzie.enabled=true +adapters.limelightDigital.aliases.filmzie.endpoint=http://localhost:8090/filmzie-exchange/{{Host}}/{{PublisherID}} adapters.lmkiviads.enabled=true adapters.lmkiviads.endpoint=http://localhost:8090/lm-kiviads-exchange/{{SourceId}}/{{Host}} adapters.lockerdome.enabled=true From 796ee5758ff5fdb2b070036764567d0a0c8d986d Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:24:05 +0200 Subject: [PATCH 020/170] Liftoff: Rename to Vungle (#3383) --- .../liftoff/model/LiftoffImpressionExt.java | 17 ------ .../VungleBidder.java} | 30 +++++----- .../vungle/model/VungleImpressionExt.java | 17 ++++++ .../ExtImpVungle.java} | 4 +- ...guration.java => VungleConfiguration.java} | 24 ++++---- .../{liftoff.yaml => vungle.yaml} | 5 +- .../{liftoff.json => vungle.json} | 4 +- .../VungleBidderTest.java} | 40 ++++++------- .../org/prebid/server/it/LiftoffTest.java | 2 +- .../java/org/prebid/server/it/VungleTest.java | 31 ++++++++++ .../vungle/test-auction-vungle-request.json | 28 +++++++++ .../vungle/test-auction-vungle-response.json | 38 ++++++++++++ .../vungle/test-vungle-bid-request.json | 58 +++++++++++++++++++ .../vungle/test-vungle-bid-response.json | 20 +++++++ .../server/it/test-application.properties | 6 +- 15 files changed, 252 insertions(+), 72 deletions(-) delete mode 100644 src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java rename src/main/java/org/prebid/server/bidder/{liftoff/LiftoffBidder.java => vungle/VungleBidder.java} (86%) create mode 100644 src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java rename src/main/java/org/prebid/server/proto/openrtb/ext/request/{liftoff/ExtImpLiftoff.java => vungle/ExtImpVungle.java} (60%) rename src/main/java/org/prebid/server/spring/config/bidder/{LiftoffConfiguration.java => VungleConfiguration.java} (57%) rename src/main/resources/bidder-config/{liftoff.yaml => vungle.yaml} (83%) rename src/main/resources/static/bidder-params/{liftoff.json => vungle.json} (89%) rename src/test/java/org/prebid/server/bidder/{liftoff/LiftoffBidderTest.java => vungle/VungleBidderTest.java} (91%) create mode 100644 src/test/java/org/prebid/server/it/VungleTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java b/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java deleted file mode 100644 index 541867ad71a..00000000000 --- a/src/main/java/org/prebid/server/bidder/liftoff/model/LiftoffImpressionExt.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.bidder.liftoff.model; - -import lombok.Builder; -import lombok.Getter; -import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; - -@Builder(toBuilder = true) -@Getter -public class LiftoffImpressionExt { - - ExtImpPrebid prebid; - - ExtImpLiftoff bidder; - - ExtImpLiftoff vungle; -} diff --git a/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java similarity index 86% rename from src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java rename to src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java index bba94b55f9c..f2c87bc2583 100644 --- a/src/main/java/org/prebid/server/bidder/liftoff/LiftoffBidder.java +++ b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.liftoff; +package org.prebid.server.bidder.vungle; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; @@ -13,18 +13,18 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.liftoff.model.LiftoffImpressionExt; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.vungle.model.VungleImpressionExt; 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.request.liftoff.ExtImpLiftoff; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -37,7 +37,7 @@ import java.util.List; import java.util.Objects; -public class LiftoffBidder implements Bidder { +public class VungleBidder implements Bidder { private static final String BIDDER_CURRENCY = "USD"; private static final String X_OPENRTB_VERSION = "2.5"; @@ -46,9 +46,9 @@ public class LiftoffBidder implements Bidder { private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public LiftoffBidder(String endpointUrl, - CurrencyConversionService currencyConversionService, - JacksonMapper mapper) { + public VungleBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.currencyConversionService = Objects.requireNonNull(currencyConversionService); @@ -63,8 +63,8 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { final Price price = resolveBidFloor(imp, bidRequest); - final LiftoffImpressionExt impExt = parseImpExt(imp); - final LiftoffImpressionExt modifiedImpExt = modifyImpExt(impExt, bidRequest); + final VungleImpressionExt impExt = parseImpExt(imp); + final VungleImpressionExt modifiedImpExt = modifyImpExt(impExt, bidRequest); final Imp modifiedImp = modifyImp(imp, modifiedImpExt, price); final BidRequest modifiedRequest = modifyBidRequest( bidRequest, @@ -92,14 +92,14 @@ private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { return Price.of(BIDDER_CURRENCY, bigDecimal); } - private LiftoffImpressionExt parseImpExt(Imp imp) { - return mapper.mapper().convertValue(imp.getExt(), LiftoffImpressionExt.class); + private VungleImpressionExt parseImpExt(Imp imp) { + return mapper.mapper().convertValue(imp.getExt(), VungleImpressionExt.class); } - private static LiftoffImpressionExt modifyImpExt(LiftoffImpressionExt impExt, BidRequest bidRequest) { - final ExtImpLiftoff bidder = impExt.getBidder(); + private static VungleImpressionExt modifyImpExt(VungleImpressionExt impExt, BidRequest bidRequest) { + final ExtImpVungle bidder = impExt.getBidder(); final String buyerId = ObjectUtil.getIfNotNull(bidRequest.getUser(), User::getBuyeruid); - final ExtImpLiftoff vungle = ExtImpLiftoff.of( + final ExtImpVungle vungle = ExtImpVungle.of( buyerId, bidder.getAppStoreId(), bidder.getPlacementReferenceId()); @@ -107,7 +107,7 @@ private static LiftoffImpressionExt modifyImpExt(LiftoffImpressionExt impExt, Bi return impExt.toBuilder().vungle(vungle).build(); } - private Imp modifyImp(Imp imp, LiftoffImpressionExt modifiedImpExt, Price price) { + private Imp modifyImp(Imp imp, VungleImpressionExt modifiedImpExt, Price price) { return imp.toBuilder() .tagid(modifiedImpExt.getBidder().getPlacementReferenceId()) .ext(mapper.mapper().convertValue(modifiedImpExt, ObjectNode.class)) diff --git a/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java b/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java new file mode 100644 index 00000000000..267d7368768 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/vungle/model/VungleImpressionExt.java @@ -0,0 +1,17 @@ +package org.prebid.server.bidder.vungle.model; + +import lombok.Builder; +import lombok.Getter; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; + +@Builder(toBuilder = true) +@Getter +public class VungleImpressionExt { + + ExtImpPrebid prebid; + + ExtImpVungle bidder; + + ExtImpVungle vungle; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java similarity index 60% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java index 5232d38c985..2b6fdfadae4 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/liftoff/ExtImpLiftoff.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vungle/ExtImpVungle.java @@ -1,9 +1,9 @@ -package org.prebid.server.proto.openrtb.ext.request.liftoff; +package org.prebid.server.proto.openrtb.ext.request.vungle; import lombok.Value; @Value(staticConstructor = "of") -public class ExtImpLiftoff { +public class ExtImpVungle { String bidToken; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java similarity index 57% rename from src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java rename to src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java index 0111858f8e5..a2bc6bd427e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/LiftoffConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/VungleConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.liftoff.LiftoffBidder; +import org.prebid.server.bidder.vungle.VungleBidder; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; @@ -17,27 +17,27 @@ import jakarta.validation.constraints.NotBlank; @Configuration -@PropertySource(value = "classpath:/bidder-config/liftoff.yaml", factory = YamlPropertySourceFactory.class) -public class LiftoffConfiguration { +@PropertySource(value = "classpath:/bidder-config/vungle.yaml", factory = YamlPropertySourceFactory.class) +public class VungleConfiguration { - private static final String BIDDER_NAME = "liftoff"; + private static final String BIDDER_NAME = "vungle"; - @Bean("liftoffConfigurationProperties") - @ConfigurationProperties("adapters.liftoff") + @Bean("vungleConfigurationProperties") + @ConfigurationProperties("adapters.vungle") BidderConfigurationProperties configurationProperties() { return new BidderConfigurationProperties(); } @Bean - BidderDeps liftoffBidderDeps(BidderConfigurationProperties liftoffConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - CurrencyConversionService currencyConversionService, - JacksonMapper mapper) { + BidderDeps vungleBidderDeps(BidderConfigurationProperties vungleConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(liftoffConfigurationProperties) + .withConfig(vungleConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new LiftoffBidder(config.getEndpoint(), currencyConversionService, mapper)) + .bidderCreator(config -> new VungleBidder(config.getEndpoint(), currencyConversionService, mapper)) .assemble(); } } diff --git a/src/main/resources/bidder-config/liftoff.yaml b/src/main/resources/bidder-config/vungle.yaml similarity index 83% rename from src/main/resources/bidder-config/liftoff.yaml rename to src/main/resources/bidder-config/vungle.yaml index 5b415c8b84a..0a9baf58403 100644 --- a/src/main/resources/bidder-config/liftoff.yaml +++ b/src/main/resources/bidder-config/vungle.yaml @@ -1,6 +1,9 @@ adapters: - liftoff: + vungle: endpoint: https://rtb.ads.vungle.com/bid/t/c770f32 + aliases: + liftoff: + enabled: false modifying-vast-xml-allowed: true endpoint-compression: gzip meta-info: diff --git a/src/main/resources/static/bidder-params/liftoff.json b/src/main/resources/static/bidder-params/vungle.json similarity index 89% rename from src/main/resources/static/bidder-params/liftoff.json rename to src/main/resources/static/bidder-params/vungle.json index 5664a883b9e..e2d4dddffdc 100644 --- a/src/main/resources/static/bidder-params/liftoff.json +++ b/src/main/resources/static/bidder-params/vungle.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Liftoff Adapter Params", - "description": "A schema which validates params accepted by the Liftoff adapter", + "title": "Vungle Adapter Params", + "description": "A schema which validates params accepted by the Vungle adapter", "type": "object", "properties": { "app_store_id": { diff --git a/src/test/java/org/prebid/server/bidder/liftoff/LiftoffBidderTest.java b/src/test/java/org/prebid/server/bidder/vungle/VungleBidderTest.java similarity index 91% rename from src/test/java/org/prebid/server/bidder/liftoff/LiftoffBidderTest.java rename to src/test/java/org/prebid/server/bidder/vungle/VungleBidderTest.java index 84e06443cad..e609e2055f8 100644 --- a/src/test/java/org/prebid/server/bidder/liftoff/LiftoffBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/vungle/VungleBidderTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.liftoff; +package org.prebid.server.bidder.vungle; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -18,17 +18,17 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.liftoff.model.LiftoffImpressionExt; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.vungle.model.VungleImpressionExt; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.liftoff.ExtImpLiftoff; +import org.prebid.server.proto.openrtb.ext.request.vungle.ExtImpVungle; import java.math.BigDecimal; import java.util.Arrays; @@ -48,23 +48,23 @@ import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @ExtendWith(MockitoExtension.class) -public class LiftoffBidderTest extends VertxTest { +public class VungleBidderTest extends VertxTest { private static final String ENDPOINT_URL = "https://test.endpoint.com"; @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; - private LiftoffBidder target; + private VungleBidder target; @BeforeEach public void setUp() { - target = new LiftoffBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + target = new VungleBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new LiftoffBidder( + assertThatIllegalArgumentException().isThrownBy(() -> new VungleBidder( "invalid_url", currencyConversionService, jacksonMapper)); @@ -176,7 +176,7 @@ public void makeHttpRequestsShouldThrowErrorWhenCurrencyConvertCannotConvertInAn } @Test - public void makeHttpRequestShouldUpdateExtImpLiftoffWhenUserBuyeruidPresent() { + public void makeHttpRequestShouldUpdateExtImpVungleWhenUserBuyeruidPresent() { // given final BidRequest bidRequest = givenBidRequest( bidRequestBuilder -> bidRequestBuilder.user(User.builder().buyeruid("123").build()), identity()); @@ -190,12 +190,12 @@ public void makeHttpRequestShouldUpdateExtImpLiftoffWhenUserBuyeruidPresent() { .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) - .containsExactly(mapper.convertValue(LiftoffImpressionExt.builder() - .bidder(ExtImpLiftoff.of( + .containsExactly(mapper.convertValue(VungleImpressionExt.builder() + .bidder(ExtImpVungle.of( "any-bid-token", "any-app-store-id", "any-placement-reference-id")) - .vungle(ExtImpLiftoff.of( + .vungle(ExtImpVungle.of( "123", "any-app-store-id", "any-placement-reference-id")) @@ -203,7 +203,7 @@ public void makeHttpRequestShouldUpdateExtImpLiftoffWhenUserBuyeruidPresent() { } @Test - public void makeHttpRequestShouldUpdateAppIdWhenExtImpLiftoffContainAppStoreId() { + public void makeHttpRequestShouldUpdateAppIdWhenExtImpVungleContainAppStoreId() { // given final App givenApp = App.builder().name("appName").build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.app(givenApp), identity()); @@ -220,7 +220,7 @@ public void makeHttpRequestShouldUpdateAppIdWhenExtImpLiftoffContainAppStoreId() } @Test - public void makeHttpRequestShouldCreateAppWithIdWhenExtImpLiftoffContainAppStoreIdAndAppIsAbsentAndSiteIsPresent() { + public void makeHttpRequestShouldCreateAppWithIdWhenExtImpVungleContainAppStoreIdAndAppIsAbsentAndSiteIsPresent() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder .site(Site.builder().build()) @@ -258,7 +258,7 @@ public void makeHttpRequestShouldReturnErrorWhenAppAndSiteAreAbsent() { } @Test - public void makeHttpRequestShouldUpdateImpTagidWhenExtImpLiftoffContainPlacementReferenceId() { + public void makeHttpRequestShouldUpdateImpTagidWhenExtImpVungleContainPlacementReferenceId() { // given final BidRequest bidRequest = givenBidRequest(identity(), identity()); @@ -275,7 +275,7 @@ public void makeHttpRequestShouldUpdateImpTagidWhenExtImpLiftoffContainPlacement } @Test - public void makeHttpRequestShouldUpdateExtImpLiftoffBidTokenWhenInRequestPresentUserBuyeruid() { + public void makeHttpRequestShouldUpdateExtImpVungleBidTokenWhenInRequestPresentUserBuyeruid() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.user(User.builder().buyeruid("Came-from-request").build()), identity()); @@ -289,11 +289,11 @@ public void makeHttpRequestShouldUpdateExtImpLiftoffBidTokenWhenInRequestPresent .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) - .containsExactly(mapper.convertValue(LiftoffImpressionExt.builder() - .bidder(ExtImpLiftoff.of("any-bid-token", + .containsExactly(mapper.convertValue(VungleImpressionExt.builder() + .bidder(ExtImpVungle.of("any-bid-token", "any-app-store-id", "any-placement-reference-id")) - .vungle(ExtImpLiftoff.of( + .vungle(ExtImpVungle.of( "Came-from-request", "any-app-store-id", "any-placement-reference-id")) @@ -386,7 +386,7 @@ private static BidRequest givenBidRequest( return bidRequestCustomizer.apply(BidRequest.builder() .app(App.builder().build()) - .imp(Arrays.stream(impCustomizer).map(LiftoffBidderTest::givenImp).toList())) + .imp(Arrays.stream(impCustomizer).map(VungleBidderTest::givenImp).toList())) .build(); } @@ -395,7 +395,7 @@ private static Imp givenImp(UnaryOperator impCustomizer) { .id("123") .banner(Banner.builder().w(23).h(25).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpLiftoff.of("any-bid-token", + ExtImpVungle.of("any-bid-token", "any-app-store-id", "any-placement-reference-id"))))) .build(); diff --git a/src/test/java/org/prebid/server/it/LiftoffTest.java b/src/test/java/org/prebid/server/it/LiftoffTest.java index 83ce56922c0..921ce6806f2 100644 --- a/src/test/java/org/prebid/server/it/LiftoffTest.java +++ b/src/test/java/org/prebid/server/it/LiftoffTest.java @@ -13,7 +13,7 @@ public class LiftoffTest extends IntegrationTest { @Test - public void openrtb2AuctionShouldRespondWithBidsFromliftoff() throws IOException, JSONException { + public void openrtb2AuctionShouldRespondWithBidsFromLiftoff() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/liftoff-exchange")) .withRequestBody(WireMock.equalToJson( diff --git a/src/test/java/org/prebid/server/it/VungleTest.java b/src/test/java/org/prebid/server/it/VungleTest.java new file mode 100644 index 00000000000..9cdd8511ce3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/VungleTest.java @@ -0,0 +1,31 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.client.WireMock; +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static java.util.Collections.singletonList; + +public class VungleTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromVungle() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/vungle-exchange")) + .withRequestBody(WireMock.equalToJson( + jsonFrom("openrtb2/vungle/test-vungle-bid-request.json"))) + .willReturn(WireMock.aResponse().withBody( + jsonFrom("openrtb2/vungle/test-vungle-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/vungle/test-auction-vungle-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/vungle/test-auction-vungle-response.json", response, singletonList("vungle")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-request.json new file mode 100644 index 00000000000..d4a33b89ed4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-request.json @@ -0,0 +1,28 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "vungle": { + "bid_token": "bid_token", + "app_store_id": "app_store_id", + "placement_reference_id": "placement_reference_id" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json new file mode 100644 index 00000000000..fa3a8579909 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json @@ -0,0 +1,38 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "adm": "", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "vungle", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "vungle": "{{ vungle.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-request.json new file mode 100644 index 00000000000..efaca813ee6 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "tagid" : "placement_reference_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder" : { + "bid_token" : "bid_token", + "app_store_id" : "app_store_id", + "placement_reference_id" : "placement_reference_id" + }, + "vungle" : { + "app_store_id" : "app_store_id", + "placement_reference_id" : "placement_reference_id" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "app": { + "id": "app_store_id" + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-response.json new file mode 100644 index 00000000000..92033dd4355 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-vungle-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "adm": "", + "crid": "crid001", + "cid": "cid001", + "h": 250, + "w": 300 + } + ] + } + ] +} 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 be7834af223..9996bae99ac 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -251,8 +251,10 @@ adapters.krushmedia.enabled=true adapters.krushmedia.endpoint=http://localhost:8090/krushmedia-exchange adapters.lemmadigital.enabled=true adapters.lemmadigital.endpoint=http://localhost:8090/lemmadigital-exchange/{{PublisherID}}/{{AdUnit}} -adapters.liftoff.enabled=true -adapters.liftoff.endpoint=http://localhost:8090/liftoff-exchange +adapters.vungle.enabled=true +adapters.vungle.endpoint=http://localhost:8090/vungle-exchange +adapters.vungle.aliases.liftoff.enabled=true +adapters.vungle.aliases.liftoff.endpoint=http://localhost:8090/liftoff-exchange adapters.kargo.enabled=true adapters.kargo.endpoint=http://localhost:8090/kargo-exchange adapters.kayzen.enabled=true From 39c7a9f21b28c81170984e0455aacc40ad433403 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:11:38 +0300 Subject: [PATCH 021/170] Core: Remove all imp.ext.prebid.imp (#3378) --- .../prebid/server/auction/ImpAdjuster.java | 11 ++-- .../functional/tests/ImpRequestSpec.groovy | 28 +++++++++ .../server/auction/ImpAdjusterTest.java | 57 ++++++------------- 3 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ImpAdjuster.java b/src/main/java/org/prebid/server/auction/ImpAdjuster.java index 32ecff524c5..86e581b06e8 100644 --- a/src/main/java/org/prebid/server/auction/ImpAdjuster.java +++ b/src/main/java/org/prebid/server/auction/ImpAdjuster.java @@ -41,18 +41,17 @@ public Imp adjust(Imp originalImp, String bidder, BidderAliases bidderAliases, L final JsonNode bidderNode = getBidderNode(bidder, bidderAliases, impExtPrebidImp); if (bidderNode == null || bidderNode.isEmpty()) { + removeImpExtPrebidImp(originalImp.getExt()); return originalImp; } - // remove circular references according to the requirements removeExtPrebidBidder(bidderNode); try { final JsonNode originalImpNode = jacksonMapper.mapper().valueToTree(originalImp); final JsonNode mergedImpNode = jsonMerger.merge(bidderNode, originalImpNode); - // clean up merged imp.ext.prebid.imp - removeImpExtPrebidImp(mergedImpNode); + removeImpExtPrebidImp(mergedImpNode.get(IMP_EXT)); final Imp resultImp = jacksonMapper.mapper().convertValue(mergedImpNode, Imp.class); @@ -61,6 +60,7 @@ public Imp adjust(Imp originalImp, String bidder, BidderAliases bidderAliases, L } catch (Exception e) { debugMessages.add("imp.ext.prebid.imp.%s can not be merged into original imp [id=%s], reason: %s" .formatted(bidder, originalImp.getId(), e.getMessage())); + removeImpExtPrebidImp(originalImp.getExt()); return originalImp; } } @@ -90,9 +90,8 @@ private static void removeExtPrebidBidder(JsonNode bidderNode) { .ifPresent(ext -> ext.remove(EXT_PREBID_BIDDER)); } - private static void removeImpExtPrebidImp(JsonNode mergedImpNode) { - Optional.ofNullable(mergedImpNode.get(IMP_EXT)) - .map(extNode -> extNode.get(EXT_PREBID)) + private static void removeImpExtPrebidImp(JsonNode impExt) { + Optional.ofNullable(impExt.get(EXT_PREBID)) .map(ObjectNode.class::cast) .ifPresent(prebid -> prebid.remove(EXT_PREBID_IMP)); } diff --git a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy index bf34515eb04..9071eea8194 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy @@ -13,6 +13,7 @@ import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_C import static org.prebid.server.functional.model.bidder.BidderName.EMPTY import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.RUBICON import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID @@ -156,6 +157,33 @@ class ImpRequestSpec extends BaseSpec { bidderName << [WILDCARD, UNKNOWN] } + def "PBS shouldn't update imp fields and without warning when imp.ext.prebid.imp contain not applicable bidder"() { + given: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.imp = [(RUBICON): new Imp(pmp: Pmp.defaultPmp)] + } + } + + when: "Requesting PBS auction" + def response = defaultPbsServiceWithAlias.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain warning" + assert !response?.ext?.warnings + + and: "BidderRequest should contain pmp from original imp" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.pmp == [impPmp] + + and: "PBS should remove imp.ext.prebid.imp from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.imp + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequest" + assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder + } + def "PBS should validate imp and add proper warning when imp.ext.prebid.imp contain invalid ortb data"() { given: "BidRequest with invalid config for ext.prebid.imp" def impPmp = Pmp.defaultPmp diff --git a/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java b/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java index 4f8e069499c..717ca62dedd 100644 --- a/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java +++ b/src/test/java/org/prebid/server/auction/ImpAdjusterTest.java @@ -78,24 +78,7 @@ public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpIsAbsent() { } @Test - public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpDoesNotHaveRequestedBidder() { - // given - final Imp givenImp = Imp.builder() - .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() - .set("imp", mapper.createObjectNode().set("anotherBidder", mapper.createObjectNode())))) - .build(); - final List debugMessages = new ArrayList<>(); - - // when - final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); - - // then - assertThat(result).isSameAs(givenImp); - assertThat(debugMessages).isEmpty(); - } - - @Test - public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpHasEmptyBidder() { + public void adjustShouldRemoveExpImpFromOriginalImpWhenImpExtPrebidImpHasEmptyBidder() { // given final Imp givenImp = Imp.builder() .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() @@ -107,24 +90,10 @@ public void adjustShouldReturnOriginalImpWhenImpExtPrebidImpHasEmptyBidder() { final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); // then - assertThat(result).isSameAs(givenImp); - assertThat(debugMessages).isEmpty(); - } - - @Test - public void adjustShouldReturnOriginalImpWhenMergedImpNodeIsEmpty() { - // given - final Imp givenImp = Imp.builder() - .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode() - .set("imp", mapper.createObjectNode().set("someBidder", mapper.createObjectNode())))) - .build(); - final List debugMessages = new ArrayList<>(); - - // when - final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); + final Imp expectedImp = givenImp.toBuilder() + .ext(mapper.createObjectNode().set("prebid", mapper.createObjectNode())).build(); - // then - assertThat(result).isSameAs(givenImp); + assertThat(result).isEqualTo(expectedImp); assertThat(debugMessages).isEmpty(); } @@ -214,7 +183,7 @@ public void resolveImpShouldMergeBidderSpecificImpIntoOriginalImpCaseAliasBidder } @Test - public void resolveImpShouldReturnOriginalImpWhenResultingImpValidationFailed() throws ValidationException { + public void resolveImpShouldReturnImpWithoutExpImpWhenResultingImpValidationFailed() throws ValidationException { // given doThrow(new ValidationException("imp validation failed")).when(impValidator).validateImp(any()); @@ -231,14 +200,19 @@ public void resolveImpShouldReturnOriginalImpWhenResultingImpValidationFailed() final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); // then - assertThat(result).isSameAs(givenImp); + final Imp expectedImp = givenImp.toBuilder() + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + assertThat(result).isEqualTo(expectedImp); assertThat(debugMessages).containsOnly( "imp.ext.prebid.imp.someBidder can not be merged into original imp [id=impId], " + "reason: imp validation failed"); } @Test - public void resolveImpShouldReturnOriginalImpWhenMergingFailed() { + public void resolveImpShouldReturnImpWithoutExpWhenMergingFailed() { // given final ObjectNode invalidBidderImp = mapper.createObjectNode() .put("bidfloor", "2.0") @@ -251,7 +225,12 @@ public void resolveImpShouldReturnOriginalImpWhenMergingFailed() { final Imp result = target.adjust(givenImp, "someBidder", bidderAliases, debugMessages); // then - assertThat(result).isSameAs(givenImp); + final Imp expectedImp = givenImp.toBuilder() + .ext(mapper.createObjectNode().put("originAttr", "originValue") + .set("prebid", mapper.createObjectNode().put("prebidOriginAttr", "prebidOriginValue"))) + .build(); + + assertThat(result).isEqualTo(expectedImp); assertThat(debugMessages).hasSize(1).first() .satisfies(message -> assertThat(message).startsWith( "imp.ext.prebid.imp.someBidder can not be merged into original imp [id=impId]," From 4f0d564c7d45d373d081847d11171e5bc2d2325c Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Wed, 21 Aug 2024 12:10:18 +0200 Subject: [PATCH 022/170] MetaX: New bidder (#3386) --- .../server/bidder/metax/MetaxBidder.java | 158 ++++++++ .../ext/request/metax/ExtImpMetax.java | 14 + .../config/bidder/MetaxConfiguration.java | 41 ++ src/main/resources/bidder-config/metax.yaml | 18 + .../resources/static/bidder-params/metax.json | 22 ++ .../server/bidder/metax/MetaxBidderTest.java | 371 ++++++++++++++++++ .../java/org/prebid/server/it/MetaxTest.java | 36 ++ .../metax/test-auction-metax-request.json | 27 ++ .../metax/test-auction-metax-response.json | 37 ++ .../metax/test-metax-bid-request.json | 60 +++ .../metax/test-metax-bid-response.json | 20 + .../server/it/test-application.properties | 2 + 12 files changed, 806 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java create mode 100644 src/main/resources/bidder-config/metax.yaml create mode 100644 src/main/resources/static/bidder-params/metax.json create mode 100644 src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/MetaxTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java new file mode 100644 index 00000000000..08b7aec984e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java @@ -0,0 +1,158 @@ +package org.prebid.server.bidder.metax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.metax.ExtImpMetax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MetaxBidder implements Bidder { + + private static final TypeReference> METAX_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{publisherId}}"; + private static final String AD_UNIT_MACRO = "{{adUnit}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MetaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMetax extImpMetax = parseImpExt(imp); + httpRequests.add(BidderUtil.defaultRequest(prepareBidRequest(request, imp), + resolveEndpoint(extImpMetax), + mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpMetax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), METAX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static BidRequest prepareBidRequest(BidRequest bidRequest, Imp imp) { + return bidRequest.toBuilder() + .imp(Collections.singletonList(modifyImp(imp))) + .build(); + } + + private static Imp modifyImp(Imp imp) { + final Banner banner = imp.getBanner(); + final Integer width = banner != null ? banner.getW() : null; + final Integer height = banner != null ? banner.getH() : null; + if (width != null && height != null) { + return imp; + } + + final List formats = banner != null ? banner.getFormat() : null; + if (CollectionUtils.isEmpty(formats)) { + return imp; + } + + final Format firstFormat = formats.getFirst(); + return imp.toBuilder() + .banner(banner.toBuilder() + .w(Optional.ofNullable(firstFormat).map(Format::getW).orElse(0)) + .h(Optional.ofNullable(firstFormat).map(Format::getH).orElse(0)) + .build()) + .build(); + } + + private String resolveEndpoint(ExtImpMetax extImpMetax) { + final String publisherIdAsString = Optional.ofNullable(extImpMetax.getPublisherId()) + .map(Object::toString) + .orElse(StringUtils.EMPTY); + final String adUnitAsString = Optional.ofNullable(extImpMetax.getAdUnit()) + .map(Object::toString) + .orElse(StringUtils.EMPTY); + + return endpointUrl + .replace(PUBLISHER_ID_MACRO, publisherIdAsString) + .replace(AD_UNIT_MACRO, adUnitAsString); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unsupported MType: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java new file mode 100644 index 00000000000..3101cb8a214 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/metax/ExtImpMetax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.metax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMetax { + + @JsonProperty("publisherId") + Integer publisherId; + + @JsonProperty("adunit") + Integer adUnit; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java new file mode 100644 index 00000000000..df258ba30cd --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MetaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.metax.MetaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/metax.yaml", factory = YamlPropertySourceFactory.class) +public class MetaxConfiguration { + + private static final String BIDDER_NAME = "metax"; + + @Bean("metaxConfigurationProperties") + @ConfigurationProperties("adapters.metax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps metaxBidderDeps(BidderConfigurationProperties metaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(metaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MetaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/metax.yaml b/src/main/resources/bidder-config/metax.yaml new file mode 100644 index 00000000000..7e87526ac0b --- /dev/null +++ b/src/main/resources/bidder-config/metax.yaml @@ -0,0 +1,18 @@ +# The MetaX Bidding adapter requires setup before beginning. Please contact us at +adapters: + metax: + endpoint: https://hb.metaxads.com/prebid?sid={{publisherId}}&adunit={{adUnit}}&source=prebid-server + meta-info: + maintainer-email: prebid@metaxsoft.com + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 1301 diff --git a/src/main/resources/static/bidder-params/metax.json b/src/main/resources/static/bidder-params/metax.json new file mode 100644 index 00000000000..5e65b5c4e2b --- /dev/null +++ b/src/main/resources/static/bidder-params/metax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MetaX Adapter Params", + "description": "A schema which validates params accepted by the MetaX adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "integer", + "description": "An ID which identifies the publisher", + "minimum": 1 + }, + "adunit": { + "type": "integer", + "description": "An ID which identifies the adunit", + "minimum": 1 + } + }, + "required": [ + "publisherId", + "adunit" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java b/src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java new file mode 100644 index 00000000000..605b56315da --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/metax/MetaxBidderTest.java @@ -0,0 +1,371 @@ +package org.prebid.server.bidder.metax; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.metax.ExtImpMetax; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +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.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class MetaxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com?sid={{publisherId}}&adunit={{adUnit}}"; + + private MetaxBidder target; + + @BeforeEach + public void setUp() { + target = new MetaxBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy( + () -> new MetaxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?sid=123&adunit=456"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final Imp givenInvalidImp = givenImp(imp -> imp + .id("impIdCorrupted") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final Imp givenValidImp = givenImp(identity()); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(givenInvalidImp, givenValidImp)) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("123"); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(identity()), givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldNotChangeBannerWidthAndHeightIfPresent() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .banner(Banner.builder() + .w(12) + .h(34) + .format(singletonList(Format.builder().w(56).h(78).build())) + .build()) + ); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBanner) + .extracting(Banner::getW, Banner::getH) + .containsExactly(Tuple.tuple(12, 34)); + } + + @Test + public void makeHttpRequestsShouldChangeBannerWidthAndHeightIfPresentInFormats() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .banner(Banner.builder() + .w(null) + .h(null) + .format(singletonList(Format.builder().w(56).h(78).build())) + .build()) + ); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBanner) + .extracting(Banner::getW, Banner::getH) + .containsExactly(Tuple.tuple(56, 78)); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(3).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(3).impid("123").build(), audio, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsNotSupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(6).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Unsupported MType: 123"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpMetax.of(123, 456))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/MetaxTest.java b/src/test/java/org/prebid/server/it/MetaxTest.java new file mode 100644 index 00000000000..f51a55e55c2 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MetaxTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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; +import static java.util.Collections.singletonList; + +public class MetaxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMetax() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/metax-exchange")) + .withQueryParam("publisher_id", equalTo("123")) + .withQueryParam("adunit", equalTo("456")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/metax/test-metax-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/metax/test-metax-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/metax/test-auction-metax-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/metax/test-auction-metax-response.json", response, + singletonList("metax")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json new file mode 100644 index 00000000000..d1393d6a4fa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-request.json @@ -0,0 +1,27 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "metax": { + "publisherId": 123, + "adunit": 456 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json new file mode 100644 index 00000000000..b37ebc3ebea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "metax", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "metax": "{{ metax.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json new file mode 100644 index 00000000000..eccf15de7bd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-request.json @@ -0,0 +1,60 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "publisherId": 123, + "adunit": 456 + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-metax-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} 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 9996bae99ac..d70804712a0 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -295,6 +295,8 @@ adapters.mediago.enabled=true adapters.mediago.endpoint=http://localhost:8090/mediago-exchange?token={{AccountID}}®ion={{Host}} adapters.medianet.enabled=true adapters.medianet.endpoint=http://localhost:8090/medianet-exchange +adapters.metax.enabled=true +adapters.metax.endpoint=http://localhost:8090/metax-exchange?publisher_id={{publisherId}}&adunit={{adUnit}} adapters.mgid.enabled=true adapters.mgid.endpoint=http://localhost:8090/mgid-exchange/ adapters.mgidX.enabled=true From b8dac64380da2279c900735bbd7e3f3a6f011e40 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:37:12 +0200 Subject: [PATCH 023/170] MeloZen: Add Bidder (#3381) --- .../server/bidder/melozen/MeloZenBidder.java | 211 +++++++++ .../ext/request/melozen/MeloZenImpExt.java | 12 + .../config/bidder/MeloZenConfiguration.java | 43 ++ src/main/resources/bidder-config/melozen.yaml | 19 + .../static/bidder-params/melozen.json | 16 + .../bidder/melozen/MeloZenBidderTest.java | 424 ++++++++++++++++++ .../org/prebid/server/it/MeloZenTest.java | 37 ++ .../melozen/test-auction-melozen-request.json | 25 ++ .../test-auction-melozen-response.json | 35 ++ .../melozen/test-melozen-bid-request.json | 58 +++ .../melozen/test-melozen-bid-response.json | 22 + .../server/it/test-application.properties | 2 + 12 files changed, 904 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java create mode 100644 src/main/resources/bidder-config/melozen.yaml create mode 100644 src/main/resources/static/bidder-params/melozen.json create mode 100644 src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/MeloZenTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java b/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java new file mode 100644 index 00000000000..226a9a60149 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/melozen/MeloZenBidder.java @@ -0,0 +1,211 @@ +package org.prebid.server.bidder.melozen; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.melozen.MeloZenImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +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.Optional; + +public class MeloZenBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String BIDDER_CURRENCY = "USD"; + private static final String EXT_PREBID = "prebid"; + + private final CurrencyConversionService currencyConversionService; + private final String endpointUrl; + private final JacksonMapper mapper; + + public MeloZenBidder(CurrencyConversionService currencyConversionService, + String endpoint, + JacksonMapper mapper) { + + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final MeloZenImpExt impExt = parseImpExt(imp); + final String url = resolveEndpoint(impExt); + final Imp modifiedImp = modifyImp(request, imp); + splitImpByMediaType(modifiedImp).forEach(splitImp -> + requests.add(BidderUtil.defaultRequest(modifyRequest(request, splitImp), url, mapper))); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private MeloZenImpExt parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp) { + final Price resolvedFloor = resolveBidFloor(bidRequest, imp); + return imp.toBuilder() + .bidfloor(resolvedFloor.getValue()) + .bidfloorcur(resolvedFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + bidFloor, + bidRequest, + bidFloorCurrency, + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedFloor); + } + + return Price.of(bidFloorCurrency, bidFloor); + } + + private String resolveEndpoint(MeloZenImpExt impExt) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getPubId()))); + } + + private List splitImpByMediaType(Imp imp) { + final Banner banner = imp.getBanner(); + final Video video = imp.getVideo(); + final Native xNative = imp.getXNative(); + + if (ObjectUtils.allNull(banner, video, xNative)) { + throw new PreBidException("Invalid MediaType. MeloZen only supports Banner, Video and Native."); + } + + final List imps = new ArrayList<>(); + + if (banner != null) { + imps.add(imp.toBuilder().video(null).xNative(null).build()); + } + + if (video != null) { + imps.add(imp.toBuilder().banner(null).xNative(null).build()); + } + + if (xNative != null) { + imps.add(imp.toBuilder().banner(null).video(null).build()); + } + + return imps; + } + + private BidRequest modifyRequest(BidRequest request, Imp imp) { + return request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + } + + @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 Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> toBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid toBidderBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get(EXT_PREBID)) + .map(ObjectNode.class::cast) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(() -> new PreBidException( + "Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid()))); + } + + private ExtBidPrebid parseExtBidPrebid(ObjectNode prebid) { + try { + return mapper.mapper().treeToValue(prebid, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java new file mode 100644 index 00000000000..ae7b45e8c86 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/melozen/MeloZenImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.melozen; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MeloZenImpExt { + + @JsonProperty("pubId") + String pubId; + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java new file mode 100644 index 00000000000..797ec11730c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MeloZenConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.melozen.MeloZenBidder; +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; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/melozen.yaml", factory = YamlPropertySourceFactory.class) +public class MeloZenConfiguration { + + private static final String BIDDER_NAME = "melozen"; + + @Bean("melozenConfigurationProperties") + @ConfigurationProperties("adapters.melozen") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps melozenBidderDeps(BidderConfigurationProperties melozenConfigurationProperties, + CurrencyConversionService currencyConversionService, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(melozenConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MeloZenBidder(currencyConversionService, config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/melozen.yaml b/src/main/resources/bidder-config/melozen.yaml new file mode 100644 index 00000000000..8626fd4eabe --- /dev/null +++ b/src/main/resources/bidder-config/melozen.yaml @@ -0,0 +1,19 @@ +adapters: + melozen: + endpoint: https://prebid.melozen.com/rtb/v2/bid?publisher_id={{PublisherID}} + endpoint-compression: gzip + modifying-vast-xml-allowed: true + geoscope: + - global + meta-info: + maintainer-email: DSP@melodong.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/melozen.json b/src/main/resources/static/bidder-params/melozen.json new file mode 100644 index 00000000000..eebd391944b --- /dev/null +++ b/src/main/resources/static/bidder-params/melozen.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "MeloZen Adapter Params", + "description": "A schema which validates params accepted by the MeloZen adapter", + "type": "object", + "properties": { + "pubId": { + "type": "string", + "minLength": 1, + "description": "The unique identifier for the publisher." + } + }, + "required": [ + "pubId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java b/src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java new file mode 100644 index 00000000000..a62b8f0ab49 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/melozen/MeloZenBidderTest.java @@ -0,0 +1,424 @@ +package org.prebid.server.bidder.melozen; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +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; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.melozen.MeloZenImpExt; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +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; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +class MeloZenBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com/{{PublisherID}}"; + + @Mock + private CurrencyConversionService currencyConversionService; + + private MeloZenBidder target; + + @BeforeEach + public void before() { + target = new MeloZenBidder(currencyConversionService, ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + // when and then + assertThatIllegalArgumentException().isThrownBy(() -> + new MeloZenBidder(currencyConversionService, "invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(bidderError -> { + assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(bidderError.getMessage()).startsWith("Cannot deserialize value"); + }); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactlyInAnyOrderElementsOf(bidRequest.getImp()); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImpPerMediaType() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .xNative(Native.builder().build()), + imp -> imp.id("givenImp2") + .banner(null) + .xNative(null) + .video(Video.builder().build())); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(4) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactlyInAnyOrder( + givenImp(imp -> imp.id("givenImp1").banner(Banner.builder().build()).video(null).xNative(null)), + givenImp(imp -> imp.id("givenImp1").banner(null).video(Video.builder().build()).xNative(null)), + givenImp(imp -> imp.id("givenImp1").banner(null).video(null).xNative(Native.builder().build())), + givenImp(imp -> imp.id("givenImp2").banner(null).video(Video.builder().build()).xNative(null))); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpDoesNotHaveBannerVideoOrNative() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impid").banner(null).video(null).xNative(null)); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsOnly( + BidderError.badInput("Invalid MediaType. MeloZen only supports Banner, Video and Native.")); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(singleton("givenImp1"), singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("impId2").banner(null).video(null).xNative(null), + imp -> imp.id("impId3")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("impId3"); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test-url.com/publisherId"); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasUSDCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, "USD")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorAndAssignUSDCurrencyWhenBidfloorIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null).bidfloorcur("EUR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(null, "EUR")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasEmptyCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur(null)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, null)); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldConvertBidfloorToUSDWhenBidfloorHasAnotherCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "EUR", "USD")) + .willReturn(BigDecimal.ONE); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ONE, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseCanNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseDoesNotHaveSeatBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse()); + + // when + final Result> actual = target.makeBids(httpCall, null); + + // then + assertThat(actual.getValue()).isEmpty(); + assertThat(actual.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidBasedOnBidExt() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "banner"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid bannerBid = Bid.builder().impid("1").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, banner, null)); + + } + + @Test + public void makeBidsShouldReturnVideoBidBasedOnBidExt() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "video"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid videoBid = Bid.builder().impid("2").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, video, null)); + } + + @Test + public void makeBidsShouldReturnNativeBidOnBidExt() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "native"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid nativeBid = Bid.builder().impid("3").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(nativeBid, xNative, null)); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidExtCanNotResolveType() throws JsonProcessingException { + // given + final ObjectNode prebidNode = mapper.createObjectNode().put("type", "unknown"); + final ObjectNode bidExtNode = mapper.createObjectNode().set("prebid", prebidNode); + final Bid bid = Bid.builder().impid("3").ext(bidExtNode).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsOnly(BidderError.badServerResponse("Failed to parse bid mediatype for impression \"3\"")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(MeloZenBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .banner(Banner.builder().build()) + .bidfloor(BigDecimal.TEN) + .bidfloorcur("USD") + .ext(mapper.valueToTree(ExtPrebid.of(null, MeloZenImpExt.of("publisherId"))))) + .build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(asList(bids)).build())) + .build()); + } + + private static Bid givenBid(UnaryOperator bidCustomizer) { + return bidCustomizer.apply(Bid.builder()).build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/it/MeloZenTest.java b/src/test/java/org/prebid/server/it/MeloZenTest.java new file mode 100644 index 00000000000..185aa894a2f --- /dev/null +++ b/src/test/java/org/prebid/server/it/MeloZenTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +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; + +public class MeloZenTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMelozen() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/melozen-exchange")) + .withQueryParam("pubId", equalTo("publisherId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/melozen/test-melozen-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/melozen/test-melozen-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/melozen/test-auction-melozen-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals("openrtb2/melozen/test-auction-melozen-response.json", response, List.of("melozen")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json new file mode 100644 index 00000000000..16518953d21 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 10, + "bidfloorcur": "USD", + "ext": { + "melozen": { + "pubId": "publisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json new file mode 100644 index 00000000000..42c6696f9bb --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "melozen", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "melozen": "{{ melozen.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json new file mode 100644 index 00000000000..c7a91804b4f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 10, + "bidfloorcur": "USD", + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "pubId": "publisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json new file mode 100644 index 00000000000..93d9130a19b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-melozen-bid-response.json @@ -0,0 +1,22 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} 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 d70804712a0..8dbad3a1089 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -295,6 +295,8 @@ adapters.mediago.enabled=true adapters.mediago.endpoint=http://localhost:8090/mediago-exchange?token={{AccountID}}®ion={{Host}} adapters.medianet.enabled=true adapters.medianet.endpoint=http://localhost:8090/medianet-exchange +adapters.melozen.enabled=true +adapters.melozen.endpoint=http://localhost:8090/melozen-exchange?pubId={{PublisherID}} adapters.metax.enabled=true adapters.metax.endpoint=http://localhost:8090/metax-exchange?publisher_id={{publisherId}}&adunit={{adUnit}} adapters.mgid.enabled=true From 5de2cb7192807c05e1401e67e40dfb4e1e16d803 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Thu, 22 Aug 2024 13:32:47 +0300 Subject: [PATCH 024/170] Workflows: Fixed GHCR workflow. (#3398) --- .github/workflows/docker-image-publish.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml index 4249a8370a9..286b03d03d3 100644 --- a/.github/workflows/docker-image-publish.yml +++ b/.github/workflows/docker-image-publish.yml @@ -1,10 +1,9 @@ name: Publish Docker image for new tag/release on: - workflow_run: - workflows: [Publish release] - types: - - completed + push: + tags: + - '*' env: REGISTRY: ghcr.io @@ -20,36 +19,42 @@ jobs: strategy: matrix: java: [ 21 ] - dockerfile-path: [Dockerfile, extra/Dockerfile] + dockerfile-path: [Dockerfile, Dockerfile-modules] include: - dockerfile-path: Dockerfile build-cmd: mvn clean package -Dcheckstyle.skip -Dmaven.test.skip=true package-name: ghcr.io/${{ github.repository }} - - dockerfile-path: extra/Dockerfile + + - dockerfile-path: Dockerfile-modules build-cmd: mvn clean package --file extra/pom.xml -Dcheckstyle.skip -Dmaven.test.skip=true package-name: ghcr.io/${{ github.repository }}-bundle steps: + - name: Check out Repository + uses: actions/checkout@v4 + - name: Set up JDK uses: actions/setup-java@v3 with: distribution: 'temurin' cache: 'maven' java-version: ${{ matrix.java }} + - name: Build .jar via Maven run: ${{ matrix.build-cmd }} - - name: Checkout repository - uses: actions/checkout@v4 + - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract metadata (tags, labels) for Docker Image id: meta uses: docker/metadata-action@v5 with: images: ${{ matrix.package-name }} + - name: Build and push Docker image uses: docker/build-push-action@v5 with: From 0f9a4091d13614521a792b478571833463d8014d Mon Sep 17 00:00:00 2001 From: serhiinahornyi Date: Thu, 22 Aug 2024 12:40:33 +0200 Subject: [PATCH 025/170] Prebid Server prepare release 3.10.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 29e21758220..d1de533e2e8 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.10.0-SNAPSHOT + 3.10.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index b4eca575054..2f7cc2086e6 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0-SNAPSHOT + 3.10.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 65a7742f291..35a035d4abe 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0-SNAPSHOT + 3.10.0 fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 32a47778840..9362c8d1cea 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0-SNAPSHOT + 3.10.0 ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 658919626ce..3de946e6ef9 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0-SNAPSHOT + 3.10.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 37a31edc344..8e466bb0433 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.10.0-SNAPSHOT + 3.10.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 64bc727fd00..4791776a741 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.10.0-SNAPSHOT + 3.10.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.10.0 diff --git a/pom.xml b/pom.xml index a097bde4b7b..ab0ac546927 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.10.0-SNAPSHOT + 3.10.0 extra/pom.xml From 422568750d79ddad5f9d51865e5d0a240f8f04ea Mon Sep 17 00:00:00 2001 From: serhiinahornyi Date: Thu, 22 Aug 2024 12:41:58 +0200 Subject: [PATCH 026/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index d1de533e2e8..64e7c93e90a 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.10.0 + 3.11.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 2f7cc2086e6..92c6e1014b8 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0 + 3.11.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 35a035d4abe..c1ff0400d11 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0 + 3.11.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 9362c8d1cea..47eca709285 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0 + 3.11.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 3de946e6ef9..12e158aa778 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.10.0 + 3.11.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 8e466bb0433..706e98f94f0 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.10.0 + 3.11.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 4791776a741..0767ffd9b21 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.10.0 + 3.11.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.10.0 + HEAD diff --git a/pom.xml b/pom.xml index ab0ac546927..b678589a9e2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.10.0 + 3.11.0-SNAPSHOT extra/pom.xml From 39e1cbf3237826a53b68ed3e56ac4194182bd015 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Fri, 23 Aug 2024 15:46:09 +0200 Subject: [PATCH 027/170] Adnuntius: Use format=prebid on adserver requests (#3401) --- .../org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java | 6 +++--- .../prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java index d6ab2363a52..2de88d8f75c 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -208,7 +208,7 @@ private List> createHttpRequests(Map eids.getFirst()) + .map(List::getFirst) .map(Eid::getUids) .filter(CollectionUtils::isNotEmpty) - .map(uids -> uids.getFirst()) + .map(List::getFirst) .map(Uid::getId)) .map(AdnuntiusMetaData::of) .orElse(null); diff --git a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java index aa7bf15349d..a89a3d7a60a 100644 --- a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java @@ -947,7 +947,7 @@ private static String givenExpectedUrl(Boolean noCookies) { } private static String buildExpectedUrl(Integer gdpr, String consent, Boolean noCookies) { - final StringBuilder expectedUri = new StringBuilder("https://test.domain.dm/uri?format=json&tzo=-300"); + final StringBuilder expectedUri = new StringBuilder("https://test.domain.dm/uri?format=prebid&tzo=-300"); if (gdpr != null) { expectedUri.append("&gdpr=").append(HttpUtil.encodeUrl(gdpr.toString())); } From 4deacab809c2faae6b60987ee4df443fd2dd2bfd Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Fri, 23 Aug 2024 15:52:10 +0200 Subject: [PATCH 028/170] Smarthub: New aliasses (`VimayX`, `FelixAds`) (#3403) --- .../resources/bidder-config/smarthub.yaml | 6 ++ .../org/prebid/server/it/FelixadsTest.java | 36 ++++++++++++ .../java/org/prebid/server/it/VimayxTest.java | 37 ++++++++++++ .../test-auction-felixads-request.json | 25 ++++++++ .../test-auction-felixads-response.json | 39 +++++++++++++ .../felixads/test-felixads-bid-request.json | 58 +++++++++++++++++++ .../felixads/test-felixads-bid-response.json | 23 ++++++++ .../vimayx/test-auction-vimayx-request.json | 25 ++++++++ .../vimayx/test-auction-vimayx-response.json | 39 +++++++++++++ .../vimayx/test-vimayx-bid-request.json | 58 +++++++++++++++++++ .../vimayx/test-vimayx-bid-response.json | 23 ++++++++ .../server/it/test-application.properties | 4 ++ 12 files changed, 373 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/FelixadsTest.java create mode 100644 src/test/java/org/prebid/server/it/VimayxTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-response.json diff --git a/src/main/resources/bidder-config/smarthub.yaml b/src/main/resources/bidder-config/smarthub.yaml index 8d8ec17d1dd..ebe51135d06 100644 --- a/src/main/resources/bidder-config/smarthub.yaml +++ b/src/main/resources/bidder-config/smarthub.yaml @@ -11,6 +11,12 @@ adapters: tredio: enabled: false endpoint: http://tredio-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + vimayx: + enabled: false + endpoint: http://vimayx-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} + felixads: + enabled: false + endpoint: http://felixads-prebid.smart-hub.io/pbserver/?seat={{AccountID}}&token={{SourceId}} meta-info: maintainer-email: support@smart-hub.io app-media-types: diff --git a/src/test/java/org/prebid/server/it/FelixadsTest.java b/src/test/java/org/prebid/server/it/FelixadsTest.java new file mode 100644 index 00000000000..30e90dd0ba5 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FelixadsTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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; +import static java.util.Collections.singletonList; + +public class FelixadsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromFelixads() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/felixads-exchange")) + .withQueryParam("host", equalTo("someUniquePartnerName")) + .withQueryParam("accountId", equalTo("someSeat")) + .withQueryParam("sourceId", equalTo("someToken")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/felixads/test-felixads-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/felixads/test-felixads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/felixads/test-auction-felixads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/felixads/test-auction-felixads-response.json", response, singletonList("felixads")); + } +} diff --git a/src/test/java/org/prebid/server/it/VimayxTest.java b/src/test/java/org/prebid/server/it/VimayxTest.java new file mode 100644 index 00000000000..2995e51d1da --- /dev/null +++ b/src/test/java/org/prebid/server/it/VimayxTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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; +import static java.util.Collections.singletonList; + +public class VimayxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromVimayx() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/vimayx-exchange")) + .withQueryParam("host", equalTo("someUniquePartnerName")) + .withQueryParam("accountId", equalTo("someSeat")) + .withQueryParam("sourceId", equalTo("someToken")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/vimayx/test-vimayx-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/vimayx/test-vimayx-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/vimayx/test-auction-vimayx-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/vimayx/test-auction-vimayx-response.json", response, singletonList("vimayx")); + } +} + diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json new file mode 100644 index 00000000000..67db491fe86 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "felixads": { + "partnerName": "someUniquePartnerName", + "seat": "someSeat", + "token": "someToken" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json new file mode 100644 index 00000000000..ae5c74865aa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "mediaType": "video", + "origbidcpm": 3.33, + "prebid": { + "type": "video" + } + } + } + ], + "seat": "felixads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "felixads": "{{ felixads.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json new file mode 100644 index 00000000000..9342b97a4fa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "partnerName": "someUniquePartnerName", + "seat": "someSeat", + "token": "someToken" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json new file mode 100644 index 00000000000..ecfbbed0ded --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-felixads-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "ext": { + "mediaType": "video" + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-request.json new file mode 100644 index 00000000000..37f89d39672 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "vimayx": { + "partnerName": "someUniquePartnerName", + "seat": "someSeat", + "token": "someToken" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json new file mode 100644 index 00000000000..64843a7c2f7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "mediaType": "video", + "origbidcpm": 3.33, + "prebid": { + "type": "video" + } + } + } + ], + "seat": "vimayx", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "vimayx": "{{ vimayx.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-request.json new file mode 100644 index 00000000000..9342b97a4fa --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-request.json @@ -0,0 +1,58 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "partnerName": "someUniquePartnerName", + "seat": "someSeat", + "token": "someToken" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-response.json new file mode 100644 index 00000000000..ecfbbed0ded --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-vimayx-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300, + "ext": { + "mediaType": "video" + } + } + ] + } + ] +} 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 8dbad3a1089..7837504196b 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -399,6 +399,10 @@ adapters.smarthub.aliases.jdpmedia.enabled=true adapters.smarthub.aliases.jdpmedia.endpoint=http://localhost:8090/jdpmedia-exchange?host={{Host}}&accountId={{AccountID}}&sourceId={{SourceId}} adapters.smarthub.aliases.tredio.enabled=true adapters.smarthub.aliases.tredio.endpoint=http://localhost:8090/tredio-exchange?host={{Host}}&accountId={{AccountID}}&sourceId={{SourceId}} +adapters.smarthub.aliases.vimayx.enabled=true +adapters.smarthub.aliases.vimayx.endpoint=http://localhost:8090/vimayx-exchange?host={{Host}}&accountId={{AccountID}}&sourceId={{SourceId}} +adapters.smarthub.aliases.felixads.enabled=true +adapters.smarthub.aliases.felixads.endpoint=http://localhost:8090/felixads-exchange?host={{Host}}&accountId={{AccountID}}&sourceId={{SourceId}} adapters.smartyads.enabled=true adapters.smartyads.endpoint=http://localhost:8090/smartyads-exchange adapters.smilewanted.enabled=true From 3e13b005b04f7d90968357011d1fc08d09ac5018 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Fri, 23 Aug 2024 15:53:19 +0200 Subject: [PATCH 029/170] Consumable: Endpoint update (#3402) --- src/main/resources/bidder-config/consumable.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/consumable.yaml b/src/main/resources/bidder-config/consumable.yaml index 6f3f8f1660e..daf074a822f 100644 --- a/src/main/resources/bidder-config/consumable.yaml +++ b/src/main/resources/bidder-config/consumable.yaml @@ -1,6 +1,6 @@ adapters: consumable: - endpoint: https://e.serverbid.com + endpoint: https://e.serverbid.com/api/v2 meta-info: maintainer-email: prebid@consumable.com app-media-types: From 56c9a4f40ea41066d90e12305a7f23b16095c725 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:40:51 +0200 Subject: [PATCH 030/170] Core: Bidders Ortb Version Update (#3406) --- src/main/resources/bidder-config/freewheelssp.yaml | 1 + src/main/resources/bidder-config/kargo.yaml | 1 + src/main/resources/bidder-config/mobilefuse.yaml | 1 + src/main/resources/bidder-config/pulsepoint.yaml | 1 + src/main/resources/bidder-config/vidazoo.yaml | 2 +- .../freewheelssp/test-freewheelssp-bid-request.json | 6 ++---- .../server/it/openrtb2/kargo/test-kargo-bid-request.json | 6 ++---- .../it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json | 6 ++---- .../test-pulsepoint-bid-request-params-as-string.json | 4 +--- .../it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json | 6 ++---- 10 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/main/resources/bidder-config/freewheelssp.yaml b/src/main/resources/bidder-config/freewheelssp.yaml index 1756a47cbd7..5a5cc21f466 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -1,6 +1,7 @@ adapters: freewheelssp: endpoint: https://ads.stickyadstv.com/openrtb/dsp + ortb-version: "2.6" modifyingVastXmlAllowed: true meta-info: maintainer-email: prebid-maintainer@freewheel.com diff --git a/src/main/resources/bidder-config/kargo.yaml b/src/main/resources/bidder-config/kargo.yaml index 164eb0a7916..c4e3f78d8e8 100644 --- a/src/main/resources/bidder-config/kargo.yaml +++ b/src/main/resources/bidder-config/kargo.yaml @@ -1,6 +1,7 @@ adapters: kargo: endpoint: https://krk.kargo.com/api/v1/openrtb + ortb-version: "2.6" endpoint-compression: gzip modifyingVastXmlAllowed: true meta-info: diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml index e16a4ac1210..ea6645feb96 100644 --- a/src/main/resources/bidder-config/mobilefuse.yaml +++ b/src/main/resources/bidder-config/mobilefuse.yaml @@ -1,6 +1,7 @@ adapters: mobilefuse: endpoint: http://mfx.mobilefuse.com/openrtb?pub_id= + ortb-version: "2.6" endpoint-compression: gzip # This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions: geoscope: diff --git a/src/main/resources/bidder-config/pulsepoint.yaml b/src/main/resources/bidder-config/pulsepoint.yaml index 3e1d27ee281..8cd407e3b74 100644 --- a/src/main/resources/bidder-config/pulsepoint.yaml +++ b/src/main/resources/bidder-config/pulsepoint.yaml @@ -1,6 +1,7 @@ adapters: pulsepoint: endpoint: http://bid.contextweb.com/header/s/ortb/prebid-s2s + ortb-version: "2.6" meta-info: maintainer-email: ExchangeTeam@pulsepoint.com app-media-types: diff --git a/src/main/resources/bidder-config/vidazoo.yaml b/src/main/resources/bidder-config/vidazoo.yaml index 727c4d74da5..f99cbb0ee1e 100644 --- a/src/main/resources/bidder-config/vidazoo.yaml +++ b/src/main/resources/bidder-config/vidazoo.yaml @@ -15,6 +15,6 @@ adapters: usersync: cookie-family-name: vidazoo iframe: - url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://sync.cootlogix.com/api/user/html/pbs_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} support-cors: false uid-macro: '${userId}' diff --git a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json index 13708ad16b1..78b3f939511 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-freewheelssp-bid-request.json @@ -38,10 +38,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json index 0c485b9fd14..db43a190b8b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-kargo-bid-request.json @@ -38,10 +38,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json index 9b41b090e34..63fb79a49a8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-mobilefuse-bid-request.json @@ -33,10 +33,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json index 4bec8bff353..26809e2c670 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request-params-as-string.json @@ -42,9 +42,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json index f41e6383c9d..21d6ddcd11e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-pulsepoint-bid-request.json @@ -41,10 +41,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { From d718aa128e6ffcc3eccf7a655eee2f5bf3eaebc8 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:42:02 +0200 Subject: [PATCH 031/170] Pubrise: Add Bidder (#3405) --- .../server/bidder/pubrise/PubriseBidder.java | 138 ++++++++ .../pubrise/proto/PubriseImpExtBidder.java | 18 + .../org/prebid/server/bidder/qt/QtBidder.java | 2 +- .../ext/request/pubrise/ExtImpPubrise.java | 14 + .../ext/request/{ => qt}/ExtImpQt.java | 2 +- .../config/bidder/PubriseConfiguration.java | 41 +++ src/main/resources/bidder-config/pubrise.yaml | 25 ++ .../static/bidder-params/pubrise.json | 30 ++ .../bidder/pubrise/PubriseBidderTest.java | 333 ++++++++++++++++++ .../prebid/server/bidder/qt/QtBidderTest.java | 2 +- .../org/prebid/server/it/PubriseTest.java | 33 ++ .../pubrise/test-auction-pubrise-request.json | 26 ++ .../test-auction-pubrise-response.json | 37 ++ .../pubrise/test-pubrise-bid-request.json | 59 ++++ .../pubrise/test-pubrise-bid-response.json | 20 ++ .../server/it/test-application.properties | 2 + 16 files changed, 779 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java rename src/main/java/org/prebid/server/proto/openrtb/ext/request/{ => qt}/ExtImpQt.java (81%) create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java create mode 100644 src/main/resources/bidder-config/pubrise.yaml create mode 100644 src/main/resources/static/bidder-params/pubrise.json create mode 100644 src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/PubriseTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java b/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java new file mode 100644 index 00000000000..f8fe37197b0 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubrise/PubriseBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.pubrise; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.pubrise.proto.PubriseImpExtBidder; +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.pubrise.ExtImpPubrise; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class PubriseBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public PubriseBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpPubrise extImp; + try { + extImp = parseImpExt(imp); + outgoingRequests.add(makeRequest(modifyImp(imp, extImp), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(outgoingRequests) + ? Result.withError(BidderError.badInput("found no valid impressions")) + : Result.of(outgoingRequests, errors); + } + + private ExtImpPubrise parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpPubrise extImp) { + final PubriseImpExtBidder impExtBidder = getImpExtWithType(extImp); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(impExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private PubriseImpExtBidder getImpExtWithType(ExtImpPubrise extImpQt) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpQt.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpQt.getEndpointId()); + + return PubriseImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpQt.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpQt.getEndpointId() : null) + .build(); + } + + private HttpRequest makeRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java b/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java new file mode 100644 index 00000000000..2cb89d4d287 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubrise/proto/PubriseImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.pubrise.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class PubriseImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/bidder/qt/QtBidder.java b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java index a53ca985d6a..4b07ff86e97 100644 --- a/src/main/java/org/prebid/server/bidder/qt/QtBidder.java +++ b/src/main/java/org/prebid/server/bidder/qt/QtBidder.java @@ -20,7 +20,7 @@ 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.ExtImpQt; +import org.prebid.server.proto.openrtb.ext.request.qt.ExtImpQt; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java new file mode 100644 index 00000000000..6cac7640123 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubrise/ExtImpPubrise.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.pubrise; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpPubrise { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java similarity index 81% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java index 084e236a063..0f5df5f144d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtImpQt.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/qt/ExtImpQt.java @@ -1,4 +1,4 @@ -package org.prebid.server.proto.openrtb.ext.request; +package org.prebid.server.proto.openrtb.ext.request.qt; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java new file mode 100644 index 00000000000..e8fd5755a58 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/PubriseConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.pubrise.PubriseBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/pubrise.yaml", factory = YamlPropertySourceFactory.class) +public class PubriseConfiguration { + + private static final String BIDDER_NAME = "pubrise"; + + @Bean("pubriseConfigurationProperties") + @ConfigurationProperties("adapters.pubrise") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps pubriseBidderDeps(BidderConfigurationProperties pubriseConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(pubriseConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new PubriseBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/pubrise.yaml b/src/main/resources/bidder-config/pubrise.yaml new file mode 100644 index 00000000000..ae7768d44b2 --- /dev/null +++ b/src/main/resources/bidder-config/pubrise.yaml @@ -0,0 +1,25 @@ +adapters: + pubrise: + endpoint: https://backend.pubrise.ai/ + meta-info: + maintainer-email: prebid@pubrise.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: pubrise + redirect: + support-cors: false + url: https://sync.pubrise.ai/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' + iframe: + support-cors: false + url: https://sync.pubrise.ai/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/static/bidder-params/pubrise.json b/src/main/resources/static/bidder-params/pubrise.json new file mode 100644 index 00000000000..9dd2a1e4c80 --- /dev/null +++ b/src/main/resources/static/bidder-params/pubrise.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Pubrise Adapter Params", + "description": "A schema which validates params accepted by the Pubrise adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java b/src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java new file mode 100644 index 00000000000..82d33a0b838 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/pubrise/PubriseBidderTest.java @@ -0,0 +1,333 @@ +package org.prebid.server.bidder.pubrise; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.pubrise.proto.PubriseImpExtBidder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.pubrise.ExtImpPubrise; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class PubriseBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final PubriseBidder target = new PubriseBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new PubriseBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("validImpId")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("validImpId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenNoValidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsOnly(BidderError.badInput("found no valid impressions")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpPubrise.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpPubrise.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("impId").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("impId").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(PubriseBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpPubrise.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExt(UnaryOperator impExt) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExt.apply(PubriseImpExtBidder.builder()).build(), + JsonNode.class)); + } +} diff --git a/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java b/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java index 660049e507a..71daee260a6 100644 --- a/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/qt/QtBidderTest.java @@ -19,7 +19,7 @@ import org.prebid.server.bidder.qt.proto.QtImpExtBidder; import org.prebid.server.bidder.qt.proto.QtImpExtBidder.QtImpExtBidderBuilder; import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.ExtImpQt; +import org.prebid.server.proto.openrtb.ext.request.qt.ExtImpQt; import java.util.Collections; import java.util.List; diff --git a/src/test/java/org/prebid/server/it/PubriseTest.java b/src/test/java/org/prebid/server/it/PubriseTest.java new file mode 100644 index 00000000000..c5ab3812e24 --- /dev/null +++ b/src/test/java/org/prebid/server/it/PubriseTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class PubriseTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromPubrise() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/pubrise-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/pubrise/test-pubrise-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/pubrise/test-pubrise-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/pubrise/test-auction-pubrise-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/pubrise/test-auction-pubrise-response.json", response, + singletonList("pubrise")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json new file mode 100644 index 00000000000..ae786238146 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "pubrise": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json new file mode 100644 index 00000000000..a49d1f99e75 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "pubrise", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "pubrise": "{{ pubrise.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-pubrise-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} 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 7837504196b..3e43d0e35e2 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -342,6 +342,8 @@ adapters.pubmatic.enabled=true adapters.pubmatic.endpoint=http://localhost:8090/pubmatic-exchange adapters.pubnative.enabled=true adapters.pubnative.endpoint=http://localhost:8090/pubnative-exchange +adapters.pubrise.enabled=true +adapters.pubrise.endpoint=http://localhost:8090/pubrise-exchange adapters.pulsepoint.enabled=true adapters.pulsepoint.endpoint=http://localhost:8090/pulsepoint-exchange adapters.pwbid.enabled=true From 88bef6143e8dd81b53406693080442ff31c4cfbb Mon Sep 17 00:00:00 2001 From: sullis Date: Mon, 26 Aug 2024 06:43:41 -0700 Subject: [PATCH 032/170] Dependencies: Wiremock 3.9.1 (#3404) --- extra/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/pom.xml b/extra/pom.xml index 0767ffd9b21..43cc49760ab 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -54,7 +54,7 @@ 1.0.7 - 3.5.4 + 3.9.1 2.4-M4-groovy-4.0 5.15.0 From e827e45c3fd286c103ef10bf40495e4437eacb96 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:24:27 +0200 Subject: [PATCH 033/170] Bugfix: Fix merging imp.ext.prebid.imp into imp (#3396) --- .../server/auction/ExchangeService.java | 15 ++++---- .../functional/tests/ImpRequestSpec.groovy | 38 +++++++++++++++++++ .../server/auction/ExchangeServiceTest.java | 28 +++++++++++--- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index d97d0d582ab..8ccada983ac 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -378,7 +378,6 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { .build(); } } - return BidRequestCacheInfo.noCache(); } @@ -981,6 +980,7 @@ private List prepareImps(String bidder, return bidRequest.getImp().stream() .filter(imp -> bidderParamsFromImpExt(imp.getExt()).hasNonNull(bidder)) + .map(imp -> imp.toBuilder().ext(imp.getExt().deepCopy()).build()) .map(imp -> impAdjuster.adjust(imp, bidder, bidderAliases, debugWarnings)) .map(imp -> prepareImp(imp, bidder, bidRequest, transmitTid, useFirstPartyData, account, debugWarnings)) .toList(); @@ -1016,18 +1016,17 @@ private ObjectNode prepareImpExt(String bidder, BigDecimal adjustedFloor, boolean transmitTid, boolean useFirstPartyData) { - - final ObjectNode modifiedImpExt = impExt.deepCopy(); + final JsonNode bidderNode = bidderParamsFromImpExt(impExt).get(bidder); final JsonNode impExtPrebid = prepareImpExt(impExt.get(PREBID_EXT), adjustedFloor); Optional.ofNullable(impExtPrebid).ifPresentOrElse( - ext -> modifiedImpExt.set(PREBID_EXT, ext), - () -> modifiedImpExt.remove(PREBID_EXT)); - modifiedImpExt.set(BIDDER_EXT, bidderParamsFromImpExt(impExt).get(bidder)); + ext -> impExt.set(PREBID_EXT, ext), + () -> impExt.remove(PREBID_EXT)); + impExt.set(BIDDER_EXT, bidderNode); if (!transmitTid) { - modifiedImpExt.remove(TID_EXT); + impExt.remove(TID_EXT); } - return fpdResolver.resolveImpExt(modifiedImpExt, useFirstPartyData); + return fpdResolver.resolveImpExt(impExt, useFirstPartyData); } private JsonNode prepareImpExt(JsonNode extImpPrebidNode, BigDecimal adjustedFloor) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy index 9071eea8194..085bd19a690 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp @@ -13,10 +14,12 @@ import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_C import static org.prebid.server.functional.model.bidder.BidderName.EMPTY import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.bidder.BidderName.RUBICON import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class ImpRequestSpec extends BaseSpec { @@ -184,6 +187,41 @@ class ImpRequestSpec extends BaseSpec { assert !bidderRequest?.imp?.first?.ext?.prebid?.bidder } + def "PBS should always update specified bidder imp when imp.ext.prebid.imp contain such bidder"() { + given: "PBs with openx bidder" + def pbsService = pbsServiceFactory.getService( + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()]) + + and: "Default basic BidRequest" + def impPmp = Pmp.defaultPmp + def extPrebidImpPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = impPmp + ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.imp = [(OPENX): new Imp(pmp: extPrebidImpPmp)] + } + } + + when: "Requesting PBS auction" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should not contain warning" + assert !response?.ext?.warnings + + and: "Generic bidderRequest should contain pmp from original imp" + def bidderToBidderRequests = getRequests(response) + assert bidderToBidderRequests[GENERIC.value].first.imp.pmp == [impPmp] + + and: "OpenX bidderRequest should contain pmp from ext.prebid.imp" + assert bidderToBidderRequests[OPENX.value].first.imp.pmp == [extPrebidImpPmp] + + and: "PBS should remove imp.ext.prebid.bidder from bidderRequests" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert !bidderRequests?.imp?.ext?.prebid?.imp?.flatten() + } + def "PBS should validate imp and add proper warning when imp.ext.prebid.imp contain invalid ortb data"() { given: "BidRequest with invalid config for ext.prebid.imp" def impPmp = Pmp.defaultPmp diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 40dd296723b..ffff9684fb1 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -186,6 +186,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -501,14 +502,20 @@ public void shouldExtractRequestWithBidderSpecificExtension() { // given givenBidder(givenEmptySeatBid()); - final BidRequest bidRequest = givenBidRequest(singletonList( - givenImp(singletonMap("someBidder", 1), builder -> builder - .id("impId") - .banner(Banner.builder() - .format(singletonList(Format.builder().w(400).h(300).build())) - .build()))), + final Imp givenImp = givenImp(singletonMap("someBidder", 1), builder -> builder + .id("impId") + .banner(Banner.builder() + .format(singletonList(Format.builder().w(400).h(300).build())) + .build())); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp), builder -> builder.id("requestId").tmax(500L)); + final ObjectNode adjustedExt = givenImp.getExt().deepCopy(); + final Imp adjustedImp = givenImp.toBuilder().ext(adjustedExt).build(); + given(impAdjuster.adjust(any(), any(), any(), any())).willReturn(adjustedImp); + // when target.holdAuction(givenRequestContext(bidRequest)); @@ -526,6 +533,15 @@ public void shouldExtractRequestWithBidderSpecificExtension() { .build())) .tmax(500L) .build()); + + final ArgumentCaptor impCaptor = forClass(Imp.class); + verify(impAdjuster).adjust(impCaptor.capture(), eq("someBidder"), any(), any()); + + final Imp actualImp = impCaptor.getValue(); + assertThat(actualImp).isNotSameAs(givenImp); + assertThat(actualImp).isEqualTo(givenImp); + assertThat(actualImp.getExt()).isNotSameAs(givenImp.getExt()); + assertThat(actualImp.getExt()).isEqualTo(givenImp.getExt()); } @Test From 56d3d8210580998b4437ef5cd4f102133815bc46 Mon Sep 17 00:00:00 2001 From: Brian Schmidt Date: Tue, 27 Aug 2024 00:54:10 -0700 Subject: [PATCH 034/170] OpenX: indicate support for OpenRTB 2.6 (#3400) --- src/main/resources/bidder-config/openx.yaml | 1 + .../server/it/openrtb2/openx/test-openx-bid-request.json | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml index 78aa8020d30..9e8454131d4 100644 --- a/src/main/resources/bidder-config/openx.yaml +++ b/src/main/resources/bidder-config/openx.yaml @@ -1,6 +1,7 @@ adapters: openx: endpoint: http://rtb.openx.net/prebid + ortb-version: "2.6" endpoint-compression: gzip meta-info: maintainer-email: prebid@openx.com diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json index fac8f6e765c..d7f4efb8bef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-openx-bid-request.json @@ -45,9 +45,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "delDomain": "se-demo-d.openx.net", From 60e20c466c6196e96e161c7f7d7bce2d8b3e001c Mon Sep 17 00:00:00 2001 From: Oleksandr Balashov Date: Tue, 27 Aug 2024 13:23:09 +0300 Subject: [PATCH 035/170] Loopme: Add Bidder (#3394) --- .../server/bidder/loopme/LoopmeBidder.java | 95 +++++++ .../ext/request/loopme/ExtImpLoopme.java | 18 +- .../config/bidder/LoopmeConfiguration.java | 40 +++ src/main/resources/bidder-config/generic.yaml | 15 -- src/main/resources/bidder-config/loopme.yaml | 23 ++ .../static/bidder-params/loopme.json | 21 +- .../testcontainers/PbsConfig.groovy | 2 - .../bidder/loopme/LoopmeBidderTest.java | 250 ++++++++++++++++++ .../loopme/test-auction-loopme-request.json | 4 +- .../loopme/test-loopme-bid-request.json | 6 +- .../server/it/test-application.properties | 4 +- 11 files changed, 442 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java create mode 100644 src/main/resources/bidder-config/loopme.yaml create mode 100644 src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java diff --git a/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java b/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java new file mode 100644 index 00000000000..3774ef5e060 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/loopme/LoopmeBidder.java @@ -0,0 +1,95 @@ +package org.prebid.server.bidder.loopme; + +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.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class LoopmeBidder implements Bidder { + + private final String endpointUrl; + + private final JacksonMapper mapper; + + public LoopmeBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(request)) + .body(mapper.encodeToBytes(request)) + .payload(request) + .build()); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse); + } + + private static List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impId, List imps) { + 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; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + } + } + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java index f572890ab72..b07a6741019 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/loopme/ExtImpLoopme.java @@ -1,16 +1,18 @@ package org.prebid.server.proto.openrtb.ext.request.loopme; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -/** - * Defines the contract for bidrequest.imp[i].ext.loopme - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpLoopme { - @JsonProperty("accountId") - String accountId; + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("bundleId") + String bundleId; + + @JsonProperty("placementId") + String placementId; + } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java new file mode 100644 index 00000000000..757f8059ed7 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/LoopmeConfiguration.java @@ -0,0 +1,40 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.loopme.LoopmeBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/loopme.yaml", factory = YamlPropertySourceFactory.class) +public class LoopmeConfiguration { + + private static final String BIDDER_NAME = "loopme"; + + @Bean("loopmeConfigurationProperties") + @ConfigurationProperties("adapters.loopme") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps loopmeBidderDeps(BidderConfigurationProperties loopmeConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(loopmeConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new LoopmeBidder(loopmeConfigurationProperties.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index 172069d1a2e..b52c7ac21b1 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -41,21 +41,6 @@ adapters: - video supported-vendors: vendor-id: 0 - loopme: - enabled: false - endpoint: http://prebid-eu.loopmertb.com - meta-info: - maintainer-email: support@loopme.com - app-media-types: - - banner - - video - - native - site-media-types: - - banner - - video - - native - supported-vendors: - vendor-id: 109 zeta_global_ssp: enabled: false endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA diff --git a/src/main/resources/bidder-config/loopme.yaml b/src/main/resources/bidder-config/loopme.yaml new file mode 100644 index 00000000000..bee027b1b18 --- /dev/null +++ b/src/main/resources/bidder-config/loopme.yaml @@ -0,0 +1,23 @@ +adapters: + loopme: + endpoint: http://prebid.loopmertb.com + meta-info: + maintainer-email: prebid@loopme.com + app-media-types: + - banner + - video + - audio + - native + site-media-types: + - banner + - video + - audio + - native + supported-vendors: + vendor-id: 109 + usersync: + url: https://csync.loopme.me/?pubid=11393&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + redirect-url: /setuid?bidder=loopme&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid={udid} + cookie-family-name: loopme + type: redirect + support-cors: false diff --git a/src/main/resources/static/bidder-params/loopme.json b/src/main/resources/static/bidder-params/loopme.json index f6b4a0a8b2e..89d95d8c011 100644 --- a/src/main/resources/static/bidder-params/loopme.json +++ b/src/main/resources/static/bidder-params/loopme.json @@ -3,13 +3,22 @@ "title": "Loopme Adapter Params", "description": "A schema which validates params accepted by the Loopme adapter", "type": "object", - "properties": { - "accountId": { + "publisherId": { "type": "string", - "description": "Account ID" + "description": "An id which identifies Loopme partner", + "minLength": 1 + }, + "bundleId": { + "type": "string", + "description": "An id which identifies app/site in Loopme", + "minLength": 1 + }, + "placementId": { + "type": "string", + "description": "A placement id in Loopme", + "minLength": 1 } }, - - "required": ["accountId"] -} \ No newline at end of file + "required": ["publisherId", "bundleId", "placementId"] +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 7598fd4cfae..aa10ebaf7a4 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -127,8 +127,6 @@ LIMIT 1 "adapters.generic.aliases.nativo.meta-info.site-media-types" : "", "adapters.generic.aliases.infytv.meta-info.app-media-types" : "", "adapters.generic.aliases.infytv.meta-info.site-media-types" : "", - "adapters.generic.aliases.loopme.meta-info.app-media-types" : "", - "adapters.generic.aliases.loopme.meta-info.site-media-types" : "", "adapters.generic.aliases.zeta-global-ssp.meta-info.app-media-types" : "", "adapters.generic.aliases.zeta-global-ssp.meta-info.site-media-types": "", "adapters.generic.aliases.ccx.meta-info.app-media-types" : "", diff --git a/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java b/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java new file mode 100644 index 00000000000..793b01f9333 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/loopme/LoopmeBidderTest.java @@ -0,0 +1,250 @@ +package org.prebid.server.bidder.loopme; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.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.loopme.ExtImpLoopme; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class LoopmeBidderTest extends VertxTest { + + public static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final LoopmeBidder loopmeBidder = new LoopmeBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy( + () -> new LoopmeBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), identity()); + + // when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Set.of("givenImp1", "givenImp2")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestForAllImps() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(Arrays.asList( + givenImp(identity()), + givenImp(identity())))); + + // when + final Result>> result = loopmeBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .hasSize(2); + } + + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfNoBannerAndHasVideo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).id("123").build())) + .build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBidIfHasBothBannerAndVideo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .build(), + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").xNative(Native.builder().build()).build())) + .build(), + mapper.writeValueAsString( + givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = loopmeBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest( + UnaryOperator impCustomizer, + UnaryOperator requestCustomizer) { + return requestCustomizer.apply(BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123")) + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpLoopme.of("somePublisherId", "someBundleId", "somePlacementId")))) + .build(); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp(HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), null); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json index ad5f545a942..bbff1db6fc9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-request.json @@ -9,7 +9,9 @@ }, "ext": { "loopme": { - "accountId": "testAccountId" + "publisherId": "testPublisherId", + "bundleId": "testBundleId", + "placementId": "testPlacementId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json index 67e2f510ea6..d669ee5881f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-loopme-bid-request.json @@ -3,15 +3,17 @@ "imp": [ { "id": "imp_id", - "secure": 1, "banner": { "w": 300, "h": 250 }, + "secure": 1, "ext": { "tid": "${json-unit.any-string}", "bidder": { - "accountId": "testAccountId" + "publisherId": "testPublisherId", + "bundleId": "testBundleId", + "placementId": "testPlacementId" } } } 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 3e43d0e35e2..50562212bd7 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -10,8 +10,6 @@ adapters.generic.aliases.ccx.enabled=true adapters.generic.aliases.ccx.endpoint=http://localhost:8090/ccx-exchange adapters.generic.aliases.infytv.enabled=true adapters.generic.aliases.infytv.endpoint=http://localhost:8090/infytv-exchange -adapters.generic.aliases.loopme.enabled=true -adapters.generic.aliases.loopme.endpoint=http://localhost:8090/loopme-exchange adapters.generic.aliases.zeta_global_ssp.enabled=true adapters.generic.aliases.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange adapters.aceex.enabled=true @@ -281,6 +279,8 @@ adapters.logan.enabled=true adapters.logan.endpoint=http://localhost:8090/logan-exchange adapters.logicad.enabled=true adapters.logicad.endpoint=http://localhost:8090/logicad-exchange +adapters.loopme.enabled=true +adapters.loopme.endpoint=http://localhost:8090/loopme-exchange adapters.loyal.enabled=true adapters.loyal.endpoint=http://localhost:8090/loyal-exchange adapters.lunamedia.enabled=true From f1065d3068edad0b5620dd7c4dec287c6c5640eb Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:24:30 +0200 Subject: [PATCH 036/170] Taboola: App Support (#3407) --- .../server/bidder/taboola/TaboolaBidder.java | 31 +++++++++--- .../bidder/taboola/TaboolaBidderTest.java | 48 ++++++++++++------- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java b/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java index c33ef88b520..cbd180f2e6f 100644 --- a/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java +++ b/src/main/java/org/prebid/server/bidder/taboola/TaboolaBidder.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -151,13 +152,24 @@ private BidRequest createRequest(BidRequest request, List imps, ExtImpTaboo final List impExtBCat = impExt.getBCat(); final String impExtPageType = impExt.getPageType(); - final Site site = Optional.ofNullable(request.getSite()) - .map(Site::toBuilder) - .orElseGet(Site::builder) + final Publisher publisher = Publisher.builder().id(impExtPublisherId).build(); + + final Site site = request.getSite(); + final Site modifiedSite = site == null + ? null + : site.toBuilder() .id(impExtPublisherId) .name(impExtPublisherId) .domain(resolveDomain(impExt.getPublisherDomain(), request)) - .publisher(Publisher.builder().id(impExtPublisherId).build()) + .publisher(publisher) + .build(); + + final App app = request.getApp(); + final App modifiedApp = app == null + ? null + : app.toBuilder() + .id(impExtPublisherId) + .publisher(publisher) .build(); final ExtRequest extRequest = StringUtils.isNotEmpty(impExtPageType) @@ -166,7 +178,8 @@ private BidRequest createRequest(BidRequest request, List imps, ExtImpTaboo return request.toBuilder() .imp(imps) - .site(site) + .site(modifiedSite) + .app(modifiedApp) .badv(CollectionUtils.isNotEmpty(impExtBAdv) ? impExtBAdv : request.getBadv()) .bcat(CollectionUtils.isNotEmpty(impExtBCat) ? impExtBCat : request.getBcat()) .ext(extRequest) @@ -189,11 +202,11 @@ private ExtRequest createExtRequest(String pageType) { private HttpRequest createHttpRequest(MediaType type, BidRequest outgoingRequest) { return BidderUtil.defaultRequest(outgoingRequest, - buildEndpointUrl(outgoingRequest.getSite().getId(), type), + buildEndpointUrl(outgoingRequest, type), mapper); } - private String buildEndpointUrl(String publisherId, MediaType mediaType) { + private String buildEndpointUrl(BidRequest bidRequest, MediaType mediaType) { final String type = switch (mediaType) { case BANNER -> DISPLAY_ENDPOINT_PREFIX; case NATIVE -> NATIVE_ENDPOINT_PREFIX; @@ -202,6 +215,10 @@ private String buildEndpointUrl(String publisherId, MediaType mediaType) { default -> throw new AssertionError(); }; + final String publisherId = Optional.ofNullable(bidRequest.getSite()).map(Site::getId) + .or(() -> Optional.ofNullable(bidRequest.getApp()).map(App::getId)) + .orElse(StringUtils.EMPTY); + return endpointTemplate .replace("{{GvlID}}", gvlId) .replace("{{MediaType}}", type) diff --git a/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java b/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java index 4406373d114..a185687bb5f 100644 --- a/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/taboola/TaboolaBidderTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; @@ -337,6 +338,7 @@ public void makeHttpRequestsShouldModifyExtIfImpExtPageTypeIsNotEmpty() { public void makeHttpRequestShouldContainProperUriWhenTypeIsBanner() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().build()), givenBannerImp(identity(), ext -> ext.publisherId("publisherId"))); // when @@ -353,6 +355,7 @@ public void makeHttpRequestShouldContainProperUriWhenTypeIsBanner() { public void makeHttpRequestShouldContainProperUriWithEncodedPublisherId() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().build()), givenBannerImp(identity(), extImp -> extImp.publisherId("not/encoded"))); // when @@ -369,6 +372,7 @@ public void makeHttpRequestShouldContainProperUriWithEncodedPublisherId() { public void makeHttpRequestShouldContainProperUriWhenTypeIsNative() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.app(App.builder().build()), givenImp(imp -> imp.xNative(Native.builder().build()), ext -> ext.publisherId("publisherId"))); // when @@ -411,30 +415,38 @@ public void makeHttpRequestShouldModifySiteDependsOnExtPublisherId() { } @Test - public void makeHttpRequestShouldModifySiteDomainIfExtPublisherDomainIsNotEmpty() { + public void makeHttpRequestShouldModifyAppDependsOnExtPublisherId() { // given + final App givenApp = App.builder() + .id("id") + .publisher(Publisher.builder().id("id").build()) + .build(); + final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().domain("domain").build()), - givenBannerImp(identity(), ext -> ext.publisherDomain("extDomain"))); + request -> request.app(givenApp), + givenBannerImp(identity(), ext -> ext.publisherId("extPublisherId"))); // when final Result>> result = target.makeHttpRequests(bidRequest); // then + final App expectedApp = givenApp.toBuilder() + .publisher(Publisher.builder().id("extPublisherId").build()) + .id("extPublisherId") + .build(); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) - .extracting(BidRequest::getSite) - .extracting(Site::getDomain) - .containsExactly("extDomain"); + .extracting(BidRequest::getApp) + .containsOnly(expectedApp); } @Test - public void makeHttpRequestShouldNotModifySiteDomainIfExtPublisherDomainIsEmpty() { + public void makeHttpRequestShouldModifySiteDomainIfExtPublisherDomainIsNotEmpty() { // given final BidRequest bidRequest = givenBidRequest( request -> request.site(Site.builder().domain("domain").build()), - givenBannerImp(identity(), ext -> ext.publisherDomain(""))); + givenBannerImp(identity(), ext -> ext.publisherDomain("extDomain"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -445,15 +457,15 @@ public void makeHttpRequestShouldNotModifySiteDomainIfExtPublisherDomainIsEmpty( .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) .extracting(Site::getDomain) - .containsExactly("domain"); + .containsExactly("extDomain"); } @Test - public void makeHttpRequestShouldAddEmptyDomainIfNoOtherSources() { + public void makeHttpRequestShouldNotModifySiteDomainIfExtPublisherDomainIsEmpty() { // given final BidRequest bidRequest = givenBidRequest( - request -> request.site(Site.builder().build()), - givenBannerImp(identity(), identity())); + request -> request.site(Site.builder().domain("domain").build()), + givenBannerImp(identity(), ext -> ext.publisherDomain(""))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -464,13 +476,15 @@ public void makeHttpRequestShouldAddEmptyDomainIfNoOtherSources() { .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) .extracting(Site::getDomain) - .containsExactly(""); + .containsExactly("domain"); } @Test - public void makeHttpRequestShouldCreateSiteIfNotPresent() { + public void makeHttpRequestShouldAddEmptyDomainIfNoOtherSources() { // given - final BidRequest bidRequest = givenBidRequest(givenBannerImp(identity(), identity())); + final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().build()), + givenBannerImp(identity(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -480,13 +494,15 @@ public void makeHttpRequestShouldCreateSiteIfNotPresent() { assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .extracting(BidRequest::getSite) - .doesNotContainNull(); + .extracting(Site::getDomain) + .containsExactly(""); } @Test public void makeHttpRequestShouldUseDataFromLastImpExtForRequest() { // given final BidRequest bidRequest = givenBidRequest( + request -> request.site(Site.builder().build()), givenBannerImp(identity(), ext -> ext.publisherId("1")), givenBannerImp(identity(), ext -> ext.publisherId("2"))); From 8102f040c53b0fc510ad5f3074859e45423c1d60 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:26:01 +0300 Subject: [PATCH 037/170] Bugfix: Incorrect usage of Promise (#3385) --- .../prebid/server/vertx/verticles/server/DaemonVerticle.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java b/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java index fe954cd3354..c6175ccaafa 100644 --- a/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java +++ b/src/main/java/org/prebid/server/vertx/verticles/server/DaemonVerticle.java @@ -33,12 +33,12 @@ public DaemonVerticle(List initializables, List startPromise) { - startPromise.handle(all(initializables, initializable -> initializable::initialize)); + all(initializables, initializable -> initializable::initialize).onComplete(startPromise); } @Override public void stop(Promise stopPromise) { - stopPromise.handle(all(closeables, closeable -> closeable::close)); + all(closeables, closeable -> closeable::close).onComplete(stopPromise); } private static Future all(Collection entries, From 317621b598365f8f2e18e0007519d6b192f83f83 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:51:27 -0400 Subject: [PATCH 038/170] Tests: Move privacy-related fields from extensions to their root objects (#2958) --- .../model/request/auction/Regs.groovy | 2 +- .../model/request/auction/RegsExt.groovy | 2 + .../server/functional/tests/AmpSpec.groovy | 4 +- .../functional/tests/BidderParamsSpec.groovy | 4 +- .../functional/tests/HttpSettingsSpec.groovy | 4 +- .../functional/tests/OrtbConverterSpec.groovy | 10 ++--- .../tests/privacy/ActivityTraceLogSpec.groovy | 5 ++- .../functional/tests/privacy/DsaSpec.groovy | 41 ++++++++++--------- .../tests/privacy/GdprAuctionSpec.groovy | 8 ++-- .../tests/privacy/GppAmpSpec.groovy | 7 ++-- .../tests/privacy/GppAuctionSpec.groovy | 9 ++-- .../privacy/GppFetchBidActivitiesSpec.groovy | 5 ++- .../GppTransmitEidsActivitiesSpec.groovy | 11 ++--- ...GppTransmitPreciseGeoActivitiesSpec.groovy | 9 ++-- .../GppTransmitUfpdActivitiesSpec.groovy | 12 +++--- .../tests/privacy/PrivacyBaseSpec.groovy | 29 ++++++------- .../TcfBasicTransmitEidsActivitiesSpec.groovy | 4 +- ...smitEidsOrtbConverterActivitiesSpec.groovy | 4 +- 18 files changed, 90 insertions(+), 80 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy index 1d8aa6f077d..6e647c16817 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Regs.groovy @@ -17,7 +17,7 @@ class Regs { static Regs getDefaultRegs() { new Regs().tap { - ext = new RegsExt(gdpr: 0) + gdpr = 0 } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy index f235dfbd600..9a9c62fde81 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy @@ -8,7 +8,9 @@ import groovy.transform.ToString @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class RegsExt { + @Deprecated(since = "enabling support of ortb 2.6") Integer gdpr + @Deprecated(since = "enabling support of ortb 2.6") String usPrivacy String gpc Dsa dsa diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy index 9205542bbb8..ac3ba146010 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -107,7 +107,7 @@ class AmpSpec extends BaseSpec { and: "Default stored request with specified: gdpr, debug" def ampStoredRequest = BidRequest.defaultStoredRequest - ampStoredRequest.regs.ext.gdpr = 1 + ampStoredRequest.regs.gdpr = 1 and: "Stored request in DB" def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) @@ -178,6 +178,6 @@ class AmpSpec extends BaseSpec { assert !bidderRequest.imp[0]?.tagId assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight - assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.ext.gdpr + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index ff1ce756ac8..8b087120db0 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -159,7 +159,7 @@ class BidderParamsSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - bidRequest.regs.ext = new RegsExt(usPrivacy: validCcpa) + bidRequest.regs.usPrivacy = validCcpa def lat = PBSUtils.getRandomDecimal(0, 90) def lon = PBSUtils.getRandomDecimal(0, 90) bidRequest.device = new Device(geo: new Geo(lat: lat, lon: lon)) @@ -186,7 +186,7 @@ class BidderParamsSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest def validCcpa = new CcpaConsent(explicitNotice: ENFORCED, optOutSale: ENFORCED) - bidRequest.regs.ext = new RegsExt(usPrivacy: validCcpa) + bidRequest.regs.usPrivacy = validCcpa def lat = PBSUtils.getRandomDecimal(0, 90) as float def lon = PBSUtils.getRandomDecimal(0, 90) as float bidRequest.device = new Device(geo: new Geo(lat: lat, lon: lon)) diff --git a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy index 99cd8831745..caabebba8ff 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy @@ -32,7 +32,7 @@ class HttpSettingsSpec extends BaseSpec { def "PBS should take account information from http data source on auction request"() { given: "Get basic BidRequest with generic bidder and set gdpr = 1" def bidRequest = BidRequest.defaultBidRequest - bidRequest.regs.ext.gdpr = 1 + bidRequest.regs.gdpr = 1 and: "Prepare default account response with gdpr = 0" def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest?.site?.publisher?.id) @@ -61,7 +61,7 @@ class HttpSettingsSpec extends BaseSpec { and: "Get basic stored request and set gdpr = 1" def ampStoredRequest = BidRequest.defaultBidRequest ampStoredRequest.site.publisher.id = ampRequest.account - ampStoredRequest.regs.ext.gdpr = 1 + ampStoredRequest.regs.gdpr = 1 and: "Save storedRequest into DB" def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 856315e17d2..6ffaedca01e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -17,6 +17,7 @@ import org.prebid.server.functional.model.request.auction.RefSettings import org.prebid.server.functional.model.request.auction.RefType import org.prebid.server.functional.model.request.auction.Refresh import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Source import org.prebid.server.functional.model.request.auction.SourceType import org.prebid.server.functional.model.request.auction.User @@ -46,10 +47,7 @@ class OrtbConverterSpec extends BaseSpec { def usPrivacyRandomString = PBSUtils.randomString def gdpr = 0 def bidRequest = BidRequest.defaultBidRequest.tap { - regs = Regs.defaultRegs.tap { - it.usPrivacy = usPrivacyRandomString - it.gdpr = gdpr - } + regs = new Regs(usPrivacy: usPrivacyRandomString, gdpr: gdpr) } when: "Requesting PBS auction with ortb 2.6" @@ -1140,7 +1138,7 @@ class OrtbConverterSpec extends BaseSpec { def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { regs = Regs.defaultRegs.tap { - ext.gpc = randomGpc + ext = new RegsExt(gpc: randomGpc) } } @@ -1156,7 +1154,7 @@ class OrtbConverterSpec extends BaseSpec { def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { regs = Regs.defaultRegs.tap { - ext.gpc = randomGpc + ext = new RegsExt(gpc: randomGpc) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy index 87c7689b4ab..309bba2eea6 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy @@ -9,6 +9,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ActivityInfrastructure import org.prebid.server.functional.model.response.auction.ActivityInvocationPayload import org.prebid.server.functional.model.response.auction.And @@ -192,7 +193,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE device = new Device(geo: new Geo(country: USA, region: ALABAMA.abbreviation)) - regs.ext.gpc = PBSUtils.randomString + regs.ext = new RegsExt(gpc: PBSUtils.randomString) regs.gppSid = [US_CA_V1.intValue] setAccountId(accountId) } @@ -298,7 +299,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.trace = VERBOSE device = new Device(geo: new Geo(country: USA, region: ALABAMA.abbreviation)) - regs.ext.gpc = PBSUtils.randomString + regs.ext = new RegsExt(gpc: PBSUtils.randomString) regs.gppSid = [US_CA_V1.intValue] regs.gpp = new UsNatV1Consent.Builder().setGpc(true).build() setAccountId(accountId) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy index 01a5a61aca6..2575789049d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/DsaSpec.groovy @@ -6,6 +6,7 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Dsa import org.prebid.server.functional.model.request.auction.Dsa as RequestDsa +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.DsaResponse @@ -34,7 +35,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -63,7 +64,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -106,7 +107,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -143,7 +144,7 @@ class DsaSpec extends PrivacyBaseSpec { and: "Default stored bid request with DSA" def ampStoredRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) setAccountId(ampRequest.account) } @@ -177,7 +178,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should always forward DSA to bidders"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) } when: "PBS processes auction request" @@ -198,7 +199,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should always accept bids with DSA"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = dsa + regs.ext = new RegsExt(dsa: dsa) } and: "Default bidder response with DSA" @@ -235,7 +236,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should accept bids without DSA when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response with DSA" @@ -263,7 +264,7 @@ class DsaSpec extends PrivacyBaseSpec { def "Auction request should reject bids without DSA when dsarequired is #dsaRequired"() { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response without DSA" @@ -293,7 +294,7 @@ class DsaSpec extends PrivacyBaseSpec { given: "Default bid request with DSA" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(dsaRequired)) } and: "Default bidder response without DSA" @@ -332,7 +333,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account with default DSA config" @@ -352,7 +353,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = requestDsa + regs.ext = new RegsExt(dsa: requestDsa) } and: "Account with default DSA config" @@ -378,7 +379,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account without default DSA config" @@ -397,8 +398,8 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null - regs.ext.gdpr = 0 + regs.ext = new RegsExt(dsa: null) + regs.gdpr = 0 } and: "Account with default DSA config" @@ -424,7 +425,7 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = getGdprBidRequest(consentString).tap { setAccountId(accountId) - regs.ext.dsa = null + regs.ext = new RegsExt(dsa: null) } and: "Account with default DSA config" @@ -448,8 +449,8 @@ class DsaSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber.toString() def bidRequest = BidRequest.defaultBidRequest.tap { setAccountId(accountId) - regs.ext.dsa = null - regs.ext.gdpr = 0 + regs.ext = new RegsExt(dsa: null) + regs.gdpr = 0 } and: "Account with default DSA config" @@ -471,9 +472,9 @@ class DsaSpec extends PrivacyBaseSpec { given: "Default bid request with DSA pubRender" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(REQUIRED).tap { + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(REQUIRED).tap { it.pubRender = pubRender - } + }) } and: "Default bidder response with incorrect DSA adRender" @@ -513,7 +514,7 @@ class DsaSpec extends PrivacyBaseSpec { given: "Default bid request with DSA pubRender" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.returnAllBidStatus = true - regs.ext.dsa = RequestDsa.getDefaultDsa(REQUIRED) + regs.ext = new RegsExt(dsa: RequestDsa.getDefaultDsa(REQUIRED)) } and: "Default bidder response with incorrect DSA" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 1e0bc5d6c75..72cd60c2b32 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -505,7 +505,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic bid requests with personal data" def tcfConsent = new TcfConsent.Builder().build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent } @@ -535,7 +535,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic BidRequests with personal data" def tcfConsent = new TcfConsent.Builder().build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent ext.prebid.trace = VERBOSE } @@ -613,7 +613,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic BidRequests with personal data" def tcfConsent = new TcfConsent.Builder().build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent ext.prebid.trace = BASIC } @@ -693,7 +693,7 @@ class GdprAuctionSpec extends PrivacyBaseSpec { given: "Default Generic BidRequests with privacy data" def tcfConsent = new TcfConsent.Builder().setSpecialFeatureOptIns(DEVICE_ACCESS).build() def bidRequest = bidRequestWithPersonalData.tap { - regs.ext = new RegsExt(gdpr: 1) + regs.gdpr = 1 user.ext.consent = tcfConsent } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy index db59d60a260..204f2eb7025 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAmpSpec.groovy @@ -5,6 +5,7 @@ import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.amp.ConsentType import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.TcfEuV2Consent import org.prebid.server.functional.util.privacy.gpp.UsV1Consent @@ -188,7 +189,7 @@ class GppAmpSpec extends PrivacyBaseSpec { and: "Save storedRequest into DB" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -212,7 +213,7 @@ class GppAmpSpec extends PrivacyBaseSpec { and: "Save storedRequest into DB" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -222,6 +223,6 @@ class GppAmpSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain gpc value from header" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - assert !bidderRequest.regs.ext + assert !bidderRequest?.regs?.ext?.gpc } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy index cef812e4cf5..4eb78ee2140 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppAuctionSpec.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.tests.privacy import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils @@ -235,7 +236,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS should populate gpc when header sec-gpc has value 1"() { given: "Default bid request with gpc" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } when: "PBS processes auction request with headers" @@ -252,7 +253,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { def "PBS shouldn't populate gpc when header sec-gpc has #gpcInvalid value"() { given: "Default bid request with gpc" def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = null + regs.ext = new RegsExt(gpc: null) } when: "PBS processes auction request with headers" @@ -260,7 +261,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { then: "Bidder request shouldn't contain gpc from header" def bidderRequests = bidder.getBidderRequest(bidRequest.id) - assert !bidderRequests.regs.ext + assert !bidderRequests?.regs?.ext?.gpc where: gpcInvalid << [PBSUtils.randomNumber as String, PBSUtils.randomNumber, PBSUtils.randomString, Boolean.TRUE] @@ -270,7 +271,7 @@ class GppAuctionSpec extends PrivacyBaseSpec { given: "Default bid request with gpc" def randomGpc = PBSUtils.randomNumber as String def bidRequest = BidRequest.defaultBidRequest.tap { - regs.ext.gpc = randomGpc + regs.ext = new RegsExt(gpc: randomGpc) } when: "PBS processes auction request with headers" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy index d92dbb89d7c..db74b18ee48 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppFetchBidActivitiesSpec.groovy @@ -16,6 +16,7 @@ import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -445,7 +446,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = randomGpc + it.regs.ext = new RegsExt(gpc: randomGpc) } and: "Setup activity" @@ -483,7 +484,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def bidRequest = BidRequest.defaultBidRequest.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup activity" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index 5bda3ca3758..42ee2290663 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -14,6 +14,7 @@ import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -447,7 +448,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -487,7 +488,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def gpc = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = gpc + it.regs.ext = new RegsExt(gpc: gpc) } and: "Setup activity" @@ -530,7 +531,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -570,7 +571,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup activity" @@ -1487,7 +1488,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { given: "Default Generic BidRequest with EIDS fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { - regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "amp request with link to account" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy index 8f785046e45..e51a9b8f193 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitPreciseGeoActivitiesSpec.groovy @@ -13,6 +13,7 @@ import org.prebid.server.functional.model.request.auction.ActivityRule import org.prebid.server.functional.model.request.auction.AllowActivities import org.prebid.server.functional.model.request.auction.Condition import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.gpp.UsCaV1Consent @@ -731,7 +732,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -793,7 +794,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { it.setAccountId(accountId) it.regs.gppSid = null it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = "1" + it.regs.ext = new RegsExt(gpc: "1") } and: "Setup activity" @@ -861,7 +862,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -922,7 +923,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def bidRequest = bidRequestWithGeo.tap { it.setAccountId(accountId) it.ext.prebid.trace = VERBOSE - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup condition" diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index 44017052970..bbb019d515d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -21,6 +21,7 @@ import org.prebid.server.functional.model.request.auction.Data import org.prebid.server.functional.model.request.auction.Device import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo +import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.request.auction.UserExtData @@ -607,7 +608,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -666,7 +667,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def gpc = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = gpc + it.regs.ext = new RegsExt(gpc: gpc) } and: "Setup activity" @@ -724,7 +725,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { given: "Generic bid request with account connection" def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { - it.regs.ext.gpc = PBSUtils.randomNumber as String + it.regs.ext = new RegsExt(gpc: PBSUtils.randomNumber as String) } and: "Setup condition" @@ -783,7 +784,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def accountId = PBSUtils.randomNumber as String def bidRequest = getBidRequestWithPersonalData(accountId).tap { it.setAccountId(accountId) - it.regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "Setup activity" @@ -1965,7 +1966,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { - regs.ext.gpc = null + it.regs.ext = new RegsExt(gpc: null) } and: "amp request with link to account" @@ -3088,6 +3089,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { it.user.gender = PBSUtils.randomString it.user.geo = Geo.FPDGeo it.user.ext = new UserExt(data: new UserExtData(buyeruid: PBSUtils.randomString)) + it.regs.ext ?= new RegsExt() } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index a5b2e12d4a5..7e5774829bd 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -23,7 +23,6 @@ import org.prebid.server.functional.model.request.auction.Eid import org.prebid.server.functional.model.request.auction.Geo import org.prebid.server.functional.model.request.auction.GeoExt import org.prebid.server.functional.model.request.auction.GeoExtGeoProvider -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.request.auction.UserExtData @@ -64,12 +63,14 @@ abstract class PrivacyBaseSpec extends BaseSpec { private static final int GEO_PRECISION = 2 - protected static final Map GENERIC_COOKIE_SYNC_CONFIG = ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : "$networkServiceContainer.rootUri/generic-usersync".toString(), - "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": false.toString()] - private static final Map OPENX_COOKIE_SYNC_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", - "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value] - private static final Map OPENX_CONFIG = ["adapters.${OPENX.value}.endpoint": "$networkServiceContainer.rootUri/auction".toString(), - "adapters.${OPENX.value}.enabled" : 'true'] + protected static final Map GENERIC_CONFIG = ["adapters.${GENERIC.value}.usersync.${REDIRECT.value}.url" : "$networkServiceContainer.rootUri/generic-usersync".toString(), + "adapters.${GENERIC.value}.usersync.${REDIRECT.value}.support-cors": false.toString(), + "adapters.${GENERIC.value}.ortb-version" : "2.6"] + private static final Map OPENX_CONFIG = ["adaptrs.${OPENX.value}.enabled" : "true", + "adapters.${OPENX.value}.usersync.cookie-family-name": OPENX.value, + "adapters.${OPENX}.ortb-version" : "2.6", + "adapters.${OPENX.value}.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.${OPENX.value}.enabled" : 'true'] protected static final Map GDPR_VENDOR_LIST_CONFIG = ["gdpr.vendorlist.v2.http-endpoint-template": "$networkServiceContainer.rootUri/v2/vendor-list.json".toString(), "gdpr.vendorlist.v3.http-endpoint-template": "$networkServiceContainer.rootUri/v3/vendor-list.json".toString()] protected static final Map SETTING_CONFIG = ["settings.enforce-valid-account": 'true'] @@ -105,10 +106,10 @@ abstract class PrivacyBaseSpec extends BaseSpec { @Shared protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG + GDPR_EEA_COUNTRY) + GENERIC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG + GDPR_EEA_COUNTRY) - protected static final Map PBS_CONFIG = OPENX_CONFIG + OPENX_COOKIE_SYNC_CONFIG + - GENERIC_COOKIE_SYNC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG + protected static final Map PBS_CONFIG = OPENX_CONFIG + + GENERIC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG @Shared protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(PBS_CONFIG) @@ -149,7 +150,7 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static BidRequest getBidRequestWithPersonalData(String accountId = null, DistributionChannel channel = SITE) { getBidRequestWithGeo(channel).tap { - if(accountId != null) { + if (accountId != null) { setAccountId(accountId) } ext.prebid.trace = VERBOSE @@ -183,14 +184,14 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static BidRequest getCcpaBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { getBidRequestWithGeo(channel).tap { - regs.ext = new RegsExt(usPrivacy: consentString) + regs.usPrivacy = consentString } } protected static BidRequest getGdprBidRequest(DistributionChannel channel = SITE, ConsentString consentString) { getBidRequestWithGeo(channel).tap { - regs.ext = new RegsExt(gdpr: 1) - user = new User(ext: new UserExt(consent: consentString)) + regs.gdpr = 1 + user = new User(consent: consentString) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy index 5ca9e9d062b..882595060fa 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfBasicTransmitEidsActivitiesSpec.groovy @@ -27,8 +27,8 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB class TcfBasicTransmitEidsActivitiesSpec extends PrivacyBaseSpec { - private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_COOKIE_SYNC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, - "gdpr.vendorlist.v3.http-endpoint-template": null] + private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, + "gdpr.vendorlist.v3.http-endpoint-template": null] private final PrebidServerService activityPbsServiceExcludeGvl = pbsServiceFactory.getService(PBS_CONFIG) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy index 8ca804717ed..d0fa0583685 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TransmitEidsOrtbConverterActivitiesSpec.groovy @@ -28,8 +28,8 @@ import static org.prebid.server.functional.model.request.auction.TraceLevel.VERB class TransmitEidsOrtbConverterActivitiesSpec extends PrivacyBaseSpec { - private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_COOKIE_SYNC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, - "gdpr.vendorlist.v3.http-endpoint-template": null] + private static final Map PBS_CONFIG = SETTING_CONFIG + GENERIC_VENDOR_CONFIG + GENERIC_CONFIG + ["gdpr.vendorlist.v2.http-endpoint-template": null, + "gdpr.vendorlist.v3.http-endpoint-template": null] private final PrebidServerService activityPbsServiceExcludeGvlWithElderOrtb = pbsServiceFactory.getService(PBS_CONFIG + ["adapters.generic.ortb-version": "2.5"]) @Shared From 3e5e980139ccd6c32d700b14db6ffb64b2474238 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:48:39 +0200 Subject: [PATCH 039/170] Adtarget and Adtelligent: Change Aid Type (#3387) --- .../bidder/adtarget/AdtargetBidder.java | 20 +++++++++--- .../bidder/adtarget/proto/AdtargetImpExt.java | 7 ++--- .../proto/ExtImpAdtargetBidRequest.java | 31 +++++++++++++++++++ .../bidder/adtelligent/AdtelligentBidder.java | 19 +++++++++--- .../adtelligent/proto/AdtelligentImpExt.java | 7 ++--- .../proto/ExtImpAdtelligentBidRequest.java | 31 +++++++++++++++++++ .../ext/request/adtarget/ExtImpAdtarget.java | 21 ++----------- .../adtelligent/ExtImpAdtelligent.java | 21 ++----------- .../static/bidder-params/adtelligent.json | 6 ++-- .../bidder/adtarget/AdtargetBidderTest.java | 17 +++++----- .../adtelligent/AdtelligentBidderTest.java | 19 ++++++------ .../validation/BidderParamValidatorTest.java | 2 +- 12 files changed, 124 insertions(+), 77 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java create mode 100644 src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java diff --git a/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java b/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java index 606fc880894..76e716325f4 100644 --- a/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java +++ b/src/main/java/org/prebid/server/bidder/adtarget/AdtargetBidder.java @@ -11,6 +11,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adtarget.proto.AdtargetImpExt; +import org.prebid.server.bidder.adtarget.proto.ExtImpAdtargetBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -67,16 +68,16 @@ private Result>> mapSourceIdToImp(List imps) { final Map> sourceToImps = new HashMap<>(); for (Imp imp : imps) { final ExtImpAdtarget extImpAdtarget; + final Integer sourceId; try { validateImpression(imp); extImpAdtarget = parseImpAdtarget(imp); + sourceId = resolveSourceId(imp.getId(), extImpAdtarget.getSourceId()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); continue; } - final Imp updatedImp = updateImp(imp, extImpAdtarget); - - final Integer sourceId = extImpAdtarget.getSourceId(); + final Imp updatedImp = updateImp(imp, sourceId, extImpAdtarget); sourceToImps.computeIfAbsent(sourceId, ignored -> new ArrayList<>()).add(updatedImp); } return Result.of(sourceToImps, errors); @@ -103,8 +104,9 @@ private static void validateImpression(Imp imp) { } } - private Imp updateImp(Imp imp, ExtImpAdtarget extImpAdtarget) { - final AdtargetImpExt adtargetImpExt = AdtargetImpExt.of(extImpAdtarget); + private Imp updateImp(Imp imp, Integer sourceId, ExtImpAdtarget extImpAdtarget) { + final AdtargetImpExt adtargetImpExt = AdtargetImpExt.of( + ExtImpAdtargetBidRequest.from(sourceId, extImpAdtarget)); final BigDecimal bidFloor = extImpAdtarget.getBidFloor(); return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) @@ -112,6 +114,14 @@ private Imp updateImp(Imp imp, ExtImpAdtarget extImpAdtarget) { .build(); } + private static Integer resolveSourceId(String impId, String sourceId) { + try { + return sourceId == null ? 0 : Integer.parseInt(sourceId); + } catch (NumberFormatException e) { + throw new PreBidException("ignoring imp id=%s, aid parsing err: %s".formatted(impId, e.getMessage())); + } + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { final List errors = new ArrayList<>(); diff --git a/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java b/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java index 4fc2177fbf0..3a2ec0278f4 100644 --- a/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java +++ b/src/main/java/org/prebid/server/bidder/adtarget/proto/AdtargetImpExt.java @@ -1,14 +1,11 @@ package org.prebid.server.bidder.adtarget.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.proto.openrtb.ext.request.adtarget.ExtImpAdtarget; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdtargetImpExt { @JsonProperty("adtarget") - ExtImpAdtarget extImpAdtarget; + ExtImpAdtargetBidRequest extImp; } diff --git a/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java b/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java new file mode 100644 index 00000000000..0abfc880e74 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtarget/proto/ExtImpAdtargetBidRequest.java @@ -0,0 +1,31 @@ +package org.prebid.server.bidder.adtarget.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.adtarget.ExtImpAdtarget; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpAdtargetBidRequest { + + @JsonProperty("aid") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; + + public static ExtImpAdtargetBidRequest from(Integer sourceId, ExtImpAdtarget impExt) { + return ExtImpAdtargetBidRequest.of( + sourceId, + impExt.getPlacementId(), + impExt.getSiteId(), + impExt.getBidFloor()); + } +} diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java b/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java index 1c88bed9005..404d83b9052 100644 --- a/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java +++ b/src/main/java/org/prebid/server/bidder/adtelligent/AdtelligentBidder.java @@ -13,6 +13,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.adtelligent.proto.AdtelligentImpExt; +import org.prebid.server.bidder.adtelligent.proto.ExtImpAdtelligentBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -86,16 +87,17 @@ private Result>> mapSourceIdToImp(List imps) { final Map> sourceToImps = new HashMap<>(); for (final Imp imp : imps) { final ExtImpAdtelligent extImpAdtelligent; + final Integer sourceId; try { validateImpression(imp); extImpAdtelligent = getExtImpAdtelligent(imp); + sourceId = resolveSourceId(imp.getId(), extImpAdtelligent.getSourceId()); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); continue; } - final Imp updatedImp = updateImp(imp, extImpAdtelligent); + final Imp updatedImp = updateImp(imp, sourceId, extImpAdtelligent); - final Integer sourceId = extImpAdtelligent.getSourceId(); final List sourceIdImps = sourceToImps.get(sourceId); if (sourceIdImps == null) { sourceToImps.put(sourceId, new ArrayList<>(Collections.singleton(updatedImp))); @@ -164,8 +166,9 @@ private void validateImpression(Imp imp) { /** * Updates {@link Imp} with bidfloor if it is present in imp.ext.bidder */ - private Imp updateImp(Imp imp, ExtImpAdtelligent extImpAdtelligent) { - final AdtelligentImpExt adtelligentImpExt = AdtelligentImpExt.of(extImpAdtelligent); + private Imp updateImp(Imp imp, Integer sourceId, ExtImpAdtelligent extImpAdtelligent) { + final AdtelligentImpExt adtelligentImpExt = AdtelligentImpExt.of( + ExtImpAdtelligentBidRequest.from(sourceId, extImpAdtelligent)); final BigDecimal bidFloor = extImpAdtelligent.getBidFloor(); return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloor) ? bidFloor : imp.getBidfloor()) @@ -173,6 +176,14 @@ private Imp updateImp(Imp imp, ExtImpAdtelligent extImpAdtelligent) { .build(); } + private static Integer resolveSourceId(String impId, String sourceId) { + try { + return sourceId == null ? 0 : Integer.parseInt(sourceId); + } catch (NumberFormatException e) { + throw new PreBidException("ignoring imp id=%s, aid parsing err: %s".formatted(impId, e.getMessage())); + } + } + /** * Extracts {@link Bid}s from response. */ diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java b/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java index ac23f7dd218..fc0bdd642a7 100644 --- a/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java +++ b/src/main/java/org/prebid/server/bidder/adtelligent/proto/AdtelligentImpExt.java @@ -1,14 +1,11 @@ package org.prebid.server.bidder.adtelligent.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import org.prebid.server.proto.openrtb.ext.request.adtelligent.ExtImpAdtelligent; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdtelligentImpExt { @JsonProperty("adtelligent") - ExtImpAdtelligent extImpAdtelligent; + ExtImpAdtelligentBidRequest extImp; } diff --git a/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java b/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java new file mode 100644 index 00000000000..e543e6a8e39 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtelligent/proto/ExtImpAdtelligentBidRequest.java @@ -0,0 +1,31 @@ +package org.prebid.server.bidder.adtelligent.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.adtelligent.ExtImpAdtelligent; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpAdtelligentBidRequest { + + @JsonProperty("aid") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; + + public static ExtImpAdtelligentBidRequest from(Integer sourceId, ExtImpAdtelligent impExt) { + return ExtImpAdtelligentBidRequest.of( + sourceId, + impExt.getPlacementId(), + impExt.getSiteId(), + impExt.getBidFloor()); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java index 04105dd73d8..268ccd077ec 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtarget/ExtImpAdtarget.java @@ -1,39 +1,22 @@ package org.prebid.server.proto.openrtb.ext.request.adtarget; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -/** - * Defines the contract for bidrequest.imp[i].ext.adtarget - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdtarget { - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.aid - */ @JsonProperty("aid") - Integer sourceId; + String sourceId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.placementId - */ @JsonProperty("placementId") Integer placementId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.siteId - */ @JsonProperty("siteId") Integer siteId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtarget.bidFloor - */ @JsonProperty("bidFloor") BigDecimal bidFloor; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java index 907932dd44b..00df38b9533 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtelligent/ExtImpAdtelligent.java @@ -1,39 +1,22 @@ package org.prebid.server.proto.openrtb.ext.request.adtelligent; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -/** - * Defines the contract for bidrequest.imp[i].ext.adtelligent - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdtelligent { - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.aid - */ @JsonProperty("aid") - Integer sourceId; + String sourceId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.placementId - */ @JsonProperty("placementId") Integer placementId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.siteId - */ @JsonProperty("siteId") Integer siteId; - /** - * Defines the contract for bidrequest.imp[i].ext.adtelligent.bidFloor - */ @JsonProperty("bidFloor") BigDecimal bidFloor; } diff --git a/src/main/resources/static/bidder-params/adtelligent.json b/src/main/resources/static/bidder-params/adtelligent.json index db7931e1ec0..e8dedf33690 100644 --- a/src/main/resources/static/bidder-params/adtelligent.json +++ b/src/main/resources/static/bidder-params/adtelligent.json @@ -2,7 +2,6 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Adtelligent Adapter Params", "description": "A schema which validates params accepted by the Adtelligent adapter", - "type": "object", "properties": { "placementId": { @@ -14,7 +13,10 @@ "description": "An ID which identifies the site selling the impression" }, "aid": { - "type": "integer", + "type": [ + "integer", + "string" + ], "description": "An ID which identifies the channel" }, "bidFloor": { diff --git a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java index acf7fdc44df..5ff7e138eed 100644 --- a/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adtarget/AdtargetBidderTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidder.adtarget.proto.AdtargetImpExt; +import org.prebid.server.bidder.adtarget.proto.ExtImpAdtargetBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -59,7 +60,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .imp(singletonList(bidRequest.getImp().getFirst().toBuilder() .bidfloor(BigDecimal.valueOf(3)) .ext(mapper.valueToTree(AdtargetImpExt.of( - ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtImpAdtargetBidRequest.of(15, 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); assertThat(result.getErrors()).isEmpty(); @@ -133,7 +134,7 @@ public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() { .id("impId2") .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -157,7 +158,7 @@ public void makeHttpRequestShouldReturnWithBidFloorPopulatedFromImpWhenIsMissedI .imp(singletonList(Imp.builder() .banner(Banner.builder().build()) .bidfloor(BigDecimal.valueOf(16)) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, null)))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, null)))) .build())) .build(); @@ -179,12 +180,12 @@ public void makeHttpRequestShouldReturnTwoHttpRequestsWhenTwoImpsHasDifferentSou .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(16, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("16", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -224,12 +225,12 @@ public void makeHttpRequestShouldReturnOneHttpRequestForTowImpsWhenImpsHasSameSo .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -409,7 +410,7 @@ private static Imp givenImp(Function impCustomiz .id("impId") .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtarget.of(15, 1, 2, BigDecimal.valueOf(3)))))) + ExtPrebid.of(null, ExtImpAdtarget.of("15", 1, 2, BigDecimal.valueOf(3)))))) .build(); } diff --git a/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java b/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java index 7b313298441..863d94806e1 100644 --- a/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adtelligent/AdtelligentBidderTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidder.adtelligent.proto.AdtelligentImpExt; +import org.prebid.server.bidder.adtelligent.proto.ExtImpAdtelligentBidRequest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; @@ -51,7 +52,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .imp(singletonList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))).build())) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))).build())) .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) .build()) @@ -77,7 +78,7 @@ public void makeHttpRequestsShouldReturnHttpRequestWithCorrectBodyHeadersAndMeth .banner(Banner.builder().build()) .bidfloor(BigDecimal.valueOf(3)) .ext(mapper.valueToTree(AdtelligentImpExt.of( - ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtImpAdtelligentBidRequest.of(15, 1, 2, BigDecimal.valueOf(3))))) .build())) .user(User.builder() .ext(ExtUser.builder().consent("consent").build()) @@ -93,7 +94,7 @@ public void makeHttpRequestShouldReturnErrorMessageWhenMediaTypeWasNotDefined() .imp(singletonList(Imp.builder() .id("impId") .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))).build())) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))).build())) .build(); // when @@ -136,7 +137,7 @@ public void makeHttpRequestShouldReturnHttpRequestWithErrorMessage() { .id("impId2") .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -160,7 +161,7 @@ public void makeHttpRequestShouldReturnWithBidFloorPopulatedFromImpWhenIsMissedI .imp(singletonList(Imp.builder() .banner(Banner.builder().build()) .bidfloor(BigDecimal.valueOf(16)) - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, null)))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, null)))) .build())) .build(); @@ -182,12 +183,12 @@ public void makeHttpRequestShouldReturnTwoHttpRequestsWhenTwoImpsHasDifferentSou .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(16, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("16", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); @@ -206,12 +207,12 @@ public void makeHttpRequestShouldReturnOneHttpRequestForTowImpsWhenImpsHasSameSo .imp(asList(Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build(), Imp.builder() .banner(Banner.builder().build()) .ext(mapper.valueToTree( - ExtPrebid.of(null, ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3))))) + ExtPrebid.of(null, ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3))))) .build())) .build(); diff --git a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java index 1c9c609932c..863ce3bb53b 100644 --- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java @@ -203,7 +203,7 @@ public void validateShouldReturnValidationMessagesWhenSovrnExtNotValid() { @Test public void validateShouldNotReturnValidationMessagesWhenAdtelligentImpExtIsOk() { // given - final ExtImpAdtelligent ext = ExtImpAdtelligent.of(15, 1, 2, BigDecimal.valueOf(3)); + final ExtImpAdtelligent ext = ExtImpAdtelligent.of("15", 1, 2, BigDecimal.valueOf(3)); final JsonNode node = mapper.convertValue(ext, JsonNode.class); From e01694643a11b38fb07789c87d74b3dce34762b5 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Mon, 2 Sep 2024 16:42:42 +0300 Subject: [PATCH 040/170] GitHub Actions: Fix release flow (#3408) --- .github/workflows/release-drafter.yml | 15 ++++----------- extra/pom.xml | 2 ++ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index b34d4827eae..c1ee08ab668 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,27 +2,20 @@ name: Publish release on: push: - branches: - - master + tags: + - '*' jobs: update_release_draft: name: Publish release with notes runs-on: ubuntu-latest - if: "contains(github.event.head_commit.message, 'Prebid Server prepare release ')" steps: - - name: Extract tag from commit message - run: | - target_tag=${COMMIT_MSG#"Prebid Server prepare release "} - echo "TARGET_TAG=$target_tag" >> $GITHUB_ENV - env: - COMMIT_MSG: ${{ github.event.head_commit.message }} - name: Create and publish release uses: release-drafter/release-drafter@v5 with: config-name: release-drafter-config.yml publish: true - name: "v${{ env.TARGET_TAG }}" - tag: ${{ env.TARGET_TAG }} + name: "v${{ github.ref_name }}" + tag: ${{ github.ref_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/extra/pom.xml b/extra/pom.xml index 43cc49760ab..4c263b6e60e 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -262,6 +262,8 @@ maven-release-plugin ${maven-release-plugin.version} + false + true @{project.version} Prebid Server From c32b9726235a5cb2fb2e54c16a8dad6e7b999faf Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:48:49 -0400 Subject: [PATCH 041/170] Prebid Server prepare release 3.11.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 64e7c93e90a..93d29760d90 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.11.0-SNAPSHOT + 3.11.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 92c6e1014b8..3787b263b27 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0-SNAPSHOT + 3.11.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index c1ff0400d11..ebe95211ba3 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0-SNAPSHOT + 3.11.0 fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 47eca709285..6532b00c05f 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0-SNAPSHOT + 3.11.0 ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 12e158aa778..d6e6a495990 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0-SNAPSHOT + 3.11.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 706e98f94f0..22b5d800f7e 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.11.0-SNAPSHOT + 3.11.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 4c263b6e60e..e08d3419a40 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.11.0-SNAPSHOT + 3.11.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.11.0 diff --git a/pom.xml b/pom.xml index b678589a9e2..e1427d4e2fc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.11.0-SNAPSHOT + 3.11.0 extra/pom.xml From 537ff6c508c952e595c688d5bc28775689b15b4e Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:48:49 -0400 Subject: [PATCH 042/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 93d29760d90..bd14db698fc 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.11.0 + 3.12.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 3787b263b27..7e3679cabb6 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0 + 3.12.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index ebe95211ba3..713641e0442 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0 + 3.12.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 6532b00c05f..4a07feabcd8 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0 + 3.12.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index d6e6a495990..97f3159d5cd 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.11.0 + 3.12.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 22b5d800f7e..64def14267f 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.11.0 + 3.12.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index e08d3419a40..b3ec8b88bc8 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.11.0 + 3.12.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.11.0 + HEAD diff --git a/pom.xml b/pom.xml index e1427d4e2fc..f47f59fb1cf 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.11.0 + 3.12.0-SNAPSHOT extra/pom.xml From af21014073d7499853692e5bbb29f1b3187494e0 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Tue, 3 Sep 2024 13:06:16 +0200 Subject: [PATCH 043/170] Rubicon: Remove `use-video-size-id-logic` processing (#3410) --- .../server/bidder/rubicon/RubiconBidder.java | 23 +--- .../config/bidder/RubiconConfiguration.java | 4 - .../bidder/rubicon/RubiconBidderTest.java | 114 +----------------- 3 files changed, 4 insertions(+), 137 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index 675d5dd4bd5..dd54fc864d8 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -188,7 +188,6 @@ public class RubiconBidder implements Bidder { private final String xapiUsername; private final Set supportedVendors; private final boolean generateBidId; - private final boolean useVideoSizeLogic; private final CurrencyConversionService currencyConversionService; private final PriceFloorResolver floorResolver; private final PrebidVersionProvider versionProvider; @@ -203,7 +202,6 @@ public RubiconBidder(String bidderName, String xapiPassword, List supportedVendors, boolean generateBidId, - boolean useVideoSizeLogic, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, PrebidVersionProvider versionProvider, @@ -215,7 +213,6 @@ public RubiconBidder(String bidderName, this.xapiUsername = Objects.requireNonNull(xapiUsername); this.supportedVendors = Set.copyOf(Objects.requireNonNull(supportedVendors)); this.generateBidId = generateBidId; - this.useVideoSizeLogic = useVideoSizeLogic; this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.floorResolver = Objects.requireNonNull(floorResolver); this.versionProvider = Objects.requireNonNull(versionProvider); @@ -1027,9 +1024,8 @@ private Video makeVideo(Imp imp, RubiconVideoParams rubiconVideoParams, String r private Integer resolveSizeId(RubiconVideoParams rubiconVideoParams, Imp imp, String referer) { final Integer sizeId = rubiconVideoParams != null ? rubiconVideoParams.getSizeId() : null; - final Video video = imp.getVideo(); final Integer resolvedSizeId = BidderUtil.isNullOrZero(sizeId) - ? useVideoSizeLogic ? resolveVideoSizeId(video.getPlacement(), imp.getInstl()) : null + ? null : sizeId; validateVideoSizeId(resolvedSizeId, referer, imp.getId()); @@ -1046,23 +1042,6 @@ private static void validateVideoSizeId(Integer resolvedSizeId, String referer, } } - private static Integer resolveVideoSizeId(Integer placement, Integer instl) { - if (placement != null) { - if (placement == 1) { - return 201; - } - if (placement == 3) { - return 203; - } - } - - if (instl != null && instl == 1) { - return 202; - } - - return null; - } - private Banner makeBanner(Imp imp) { final Banner banner = imp.getBanner(); final Integer width = banner.getW(); diff --git a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java index d78f5324c01..89f80e439aa 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/RubiconConfiguration.java @@ -56,7 +56,6 @@ BidderDeps rubiconBidderDeps(RubiconConfigurationProperties rubiconConfiguration config.getXapi().getPassword(), config.getMetaInfo().getSupportedVendors(), config.getGenerateBidId(), - config.getUseVideoSizeIdLogic(), currencyConversionService, floorResolver, versionProvider, @@ -76,9 +75,6 @@ private static class RubiconConfigurationProperties extends BidderConfigurationP @NotNull private Boolean generateBidId; - - @NotNull - private Boolean useVideoSizeIdLogic; } @Data diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index b7c2a053298..47a0694ef93 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -176,7 +176,6 @@ public void setUp() { PASSWORD, SUPPORTED_VENDORS, false, - true, currencyConversionService, priceFloorResolver, versionProvider, @@ -195,7 +194,6 @@ public void creationShouldFailOnInvalidEndpointUrl() { PASSWORD, SUPPORTED_VENDORS, false, - true, currencyConversionService, priceFloorResolver, versionProvider, @@ -865,7 +863,7 @@ public void makeHttpRequestsShouldIgnoreBidRequestIfCurrencyServiceThrowsAnExcep } @Test - public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsIsMissingSizeId() { + public void shouldNotSetSizeIfBidderParamsIsMissingSizeId() { // given target = new RubiconBidder( BIDDER_NAME, @@ -875,7 +873,6 @@ public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsI PASSWORD, SUPPORTED_VENDORS, true, - false, currencyConversionService, priceFloorResolver, versionProvider, @@ -897,111 +894,6 @@ public void shouldNotSetSizeIfVideoSizeProcessingLogicIsDisabledAndBidderParamsI .containsOnlyNulls(); } - @Test - public void shouldSetSizeFromBidderParamsWhenVideoSizeProcessingLogicIsDisabled() { - // given - target = new RubiconBidder( - BIDDER_NAME, - ENDPOINT_URL, - EXTERNAL_URL, - USERNAME, - PASSWORD, - SUPPORTED_VENDORS, - true, - false, - currencyConversionService, - priceFloorResolver, - versionProvider, - jacksonMapper); - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(1).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(14).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(14); - } - - @Test - public void shouldSetSizeIdTo201IfPlacementIs1AndSizeIdIsNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(1).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(201); - } - - @Test - public void shouldSetSizeIdTo203IfPlacementIs3AndSizeIdIsNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(3).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(203); - } - - @Test - public void shouldSetSizeIdTo202UsingInstlFlagIfPlacementAndSizeIdAreNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest( - builder -> builder.instl(1).video(Video.builder().placement(null).build()), - builder -> builder.video(RubiconVideoParams.builder().sizeId(null).build())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1).doesNotContainNull() - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getVideo).doesNotContainNull() - .extracting(Video::getExt).doesNotContainNull() - .extracting(ext -> mapper.treeToValue(ext, RubiconVideoExt.class)) - .extracting(RubiconVideoExt::getRp) - .extracting(RubiconVideoExtRp::getSizeId) - .containsOnly(202); - } - @Test public void makeHttpRequestsShouldFillVideoExt() { // given @@ -3785,7 +3677,7 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessi public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessingException { // given target = new RubiconBidder( - BIDDER_NAME, ENDPOINT_URL, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, + BIDDER_NAME, ENDPOINT_URL, ENDPOINT_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, currencyConversionService, priceFloorResolver, versionProvider, jacksonMapper); final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), @@ -3811,7 +3703,7 @@ public void makeBidsShouldReturnBidWithRandomlyGeneratedId() throws JsonProcessi public void makeBidsShouldReturnBidWithCurrencyFromBidResponse() throws JsonProcessingException { // given target = new RubiconBidder( - BIDDER_NAME, ENDPOINT_URL, EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, true, + BIDDER_NAME, ENDPOINT_URL, EXTERNAL_URL, USERNAME, PASSWORD, SUPPORTED_VENDORS, true, currencyConversionService, priceFloorResolver, versionProvider, jacksonMapper); final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), From cfbb6fb21ba5593ce113cf103bc4130f7008364b Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Wed, 4 Sep 2024 11:49:12 +0200 Subject: [PATCH 044/170] Core: Bidder accepted currency functionality (#3416) --- .../Ortb2BlockingBidderRequestHookTest.java | 1 + .../server/auction/ExchangeService.java | 152 +++------- .../auction/model/BidRejectionReason.java | 5 + .../org/prebid/server/bidder/BidderInfo.java | 4 + .../spring/config/bidder/model/MetaInfo.java | 2 + .../config/bidder/util/BidderInfoCreator.java | 1 + src/main/resources/bidder-config/generic.yaml | 6 +- .../auction/BidRejectionReason.groovy | 1 + .../functional/tests/BidderParamsSpec.groovy | 273 ++++++++++++++++-- .../server/auction/ExchangeServiceTest.java | 59 ++++ .../BidderMediaTypeProcessorTest.java | 1 + .../MultiFormatMediaTypeProcessorTest.java | 1 + .../enforcement/CcpaEnforcementTest.java | 2 + .../server/bidder/BidderCatalogTest.java | 8 + .../bidder/HttpBidderRequestEnricherTest.java | 2 + .../info/BidderDetailsHandlerTest.java | 1 + .../validation/BidderParamValidatorTest.java | 1 + 17 files changed, 388 insertions(+), 132 deletions(-) diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index a66f94ac52c..cb4e793f4fc 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -258,6 +258,7 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) { null, null, 0, + null, false, false, null, diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 8ccada983ac..2dbf1a6528f 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -50,6 +50,7 @@ import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; import org.prebid.server.bidder.HttpBidderRequester; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.bidder.model.BidderBid; @@ -296,7 +297,7 @@ private Future runAuction(AuctionContext receivedContext) { .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses)) .compose(storedResponseResult -> extractAuctionParticipations(receivedContext, storedResponseResult, aliases, bidderToMultiBid) - .map(receivedContext::with)) + .map(receivedContext::with)) .map(context -> updateRequestMetric(context, uidsCookie, aliases, account, requestTypeMetric)) .compose(context -> CompositeFuture.join( @@ -470,44 +471,12 @@ private Map makeBidRejectionTrackers(BidRequest bid entry -> new BidRejectionTracker(entry.getKey(), entry.getValue(), logSamplingRate))); } - /** - * Populates storedResponse parameter with stored {@link List} and returns {@link List} for which - * request to bidders should be performed. - */ private static StoredResponseResult populateStoredResponse(StoredResponseResult storedResponseResult, List storedResponse) { storedResponse.addAll(storedResponseResult.getAuctionStoredResponse()); return storedResponseResult; } - /** - * Takes an OpenRTB request and returns the OpenRTB requests sanitized for each bidder. - *

- * This will copy the {@link BidRequest} into a list of requests, where the bidRequest.imp[].ext field - * will only consist of the "prebid" field and the field for the appropriate bidder parameters. We will drop all - * extended fields beyond this context, so this will not be compatible with any other uses of the extension area - * i.e. the bidders will not see any other extension fields. If Imp extension name is alias, which is also defined - * in bidRequest.ext.prebid.aliases and valid, separate {@link BidRequest} will be created for this alias and sent - * to appropriate bidder. - * For example suppose {@link BidRequest} has two {@link Imp}s. First one with imp.ext.prebid.bidder.rubicon and - * imp.ext.prebid.bidder.rubiconAlias and second with imp.ext.prebid.bidder.appnexus and - * imp.ext.prebid.bidder.rubicon. Three {@link BidRequest}s will be created: - * 1. {@link BidRequest} with one {@link Imp}, where bidder extension points to rubiconAlias extension and will be - * sent to Rubicon bidder. - * 2. {@link BidRequest} with two {@link Imp}s, where bidder extension points to appropriate rubicon extension from - * original {@link BidRequest} and will be sent to Rubicon bidder. - * 3. {@link BidRequest} with one {@link Imp}, where bidder extension points to appnexus extension and will be sent - * to Appnexus bidder. - *

- * Each of the created {@link BidRequest}s will have bidrequest.user.buyerid field populated with the value from - * bidrequest.user.ext.prebid.buyerids or {@link UidsCookie} corresponding to bidder's family name unless buyerid - * is already in the original OpenRTB request (in this case it will not be overridden). - * In case if bidrequest.user.ext.prebid.buyerids contains values after extracting those values it will be cleared - * in order to avoid leaking of buyerids across bidders. - *

- * NOTE: the return list will only contain entries for bidders that both have the extension field in at least one - * {@link Imp}, and are known to {@link BidderCatalog} or aliases from bidRequest.ext.prebid.aliases. - */ private Future> extractAuctionParticipations( AuctionContext context, StoredResponseResult storedResponseResult, @@ -546,9 +515,6 @@ private static JsonNode bidderParamsFromImpExt(ObjectNode ext) { return ext.get(PREBID_EXT).get(BIDDER_EXT); } - /** - * Checks if bidder name is valid in case when bidder can also be alias name. - */ private boolean isValidBidder(String bidder, BidderAliases aliases) { return bidderCatalog.isValidName(bidder) || aliases.isAliasDefined(bidder); } @@ -564,21 +530,6 @@ private static boolean isBidderCallActivityAllowed(String bidder, AuctionContext activityInvocationPayload); } - /** - * Splits the input request into requests which are sanitized for each bidder. Intended behavior is: - *

- * - bidrequest.imp[].ext will only contain the "prebid" field and a "bidder" field which has the params for - * the intended Bidder. - *

- * - bidrequest.user.buyeruid will be set to that Bidder's ID. - *

- * - bidrequest.ext.prebid.data.bidders will be removed. - *

- * - bidrequest.ext.prebid.bidders will be staying in corresponding bidder only. - *

- * - bidrequest.user.ext.data, bidrequest.app.ext.data, bidrequest.dooh.ext.data and bidrequest.site.ext.data - * will be removed for bidders that don't have first party data allowed. - */ private Future> makeAuctionParticipation( List bidders, AuctionContext context, @@ -631,10 +582,6 @@ private Map getBiddersToConfigs(ExtRequestPrebid pr return bidderToConfig; } - /** - * Retrieves user eids from {@link ExtRequestPrebid} and converts them to map, where keys are eids sources - * and values are allowed bidders - */ private Map> getEidPermissions(ExtRequestPrebid prebid) { final ExtRequestPrebidData prebidData = prebid != null ? prebid.getData() : null; final List eidPermissions = prebidData != null @@ -645,9 +592,6 @@ private Map> getEidPermissions(ExtRequestPrebid prebid) { ExtRequestPrebidDataEidPermissions::getBidders)); } - /** - * Extracts a list of bidders for which first party data is allowed from {@link ExtRequestPrebidData} model. - */ private static List firstPartyDataBidders(ExtRequest requestExt) { final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final ExtRequestPrebidData data = prebid == null ? null : prebid.getData(); @@ -676,13 +620,6 @@ private Map prepareUsers(List bidders, return bidderToUser; } - /** - * Returns original {@link User} if user.buyeruid already contains uid value for bidder. - * Otherwise, returns new {@link User} containing updated {@link ExtUser} and user.buyeruid. - *

- * Also, removes user.ext.prebid (if present), user.ext.data and user.data (in case bidder does not use first - * party data). - */ private User prepareUser(String bidder, AuctionContext context, BidderAliases aliases, @@ -734,9 +671,6 @@ private List extractUserEids(User user) { return user != null ? user.getEids() : null; } - /** - * Returns {@link List} allowed by {@param eidPermissions} per source per bidder. - */ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions) { return CollectionUtils.emptyIfNull(userEids) .stream() @@ -744,10 +678,6 @@ private List resolveAllowedEids(List userEids, String bidder, Map> eidPermissions, String bidder) { final List allowedBidders = eidPermissions.get(source); return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() @@ -755,9 +685,6 @@ private boolean isUserEidAllowed(String source, Map> eidPer || EID_ALLOWED_FOR_ALL_BIDDERS.equals(allowedBidder)); } - /** - * Returns shuffled list of {@link AuctionParticipation} with {@link BidRequest}. - */ private List getAuctionParticipation( List bidderPrivacyResults, BidRequest bidRequest, @@ -810,9 +737,6 @@ private static Map bidderToPrebidBidders(BidRequest bidRequest return bidderToPrebidParameters; } - /** - * Returns {@link AuctionParticipation} for the given bidder. - */ private AuctionParticipation createAuctionParticipation( BidderPrivacyResult bidderPrivacyResult, Map> impBidderToStoredBidResponse, @@ -1237,16 +1161,20 @@ private Future processAndRequestBids(AuctionContext auctionConte final String bidderName = bidderRequest.getBidder(); final MediaTypeProcessingResult mediaTypeProcessingResult = mediaTypeProcessor.process( bidderRequest.getBidRequest(), bidderName, aliases, auctionContext.getAccount()); - final List mediaTypeProcessingErrors = mediaTypeProcessingResult.getErrors(); if (mediaTypeProcessingResult.isRejected()) { - auctionContext.getBidRejectionTrackers() - .get(bidderName) - .rejectAll(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE); - final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() - .warnings(mediaTypeProcessingErrors) - .build(); - return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, + mediaTypeProcessingErrors, + bidderName); + } + if (isUnacceptableCurrency(auctionContext, aliases.resolveBidder(bidderName))) { + return processReject( + auctionContext, + BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, + List.of(BidderError.generic("No match between the configured currencies and bidRequest.cur")), + bidderName); } return Future.succeededFuture(mediaTypeProcessingResult.getBidRequest()) @@ -1257,6 +1185,34 @@ private Future processAndRequestBids(AuctionContext auctionConte addWarnings(bidderResponse.getSeatBid(), mediaTypeProcessingErrors))); } + private boolean isUnacceptableCurrency(AuctionContext auctionContext, String originalBidderName) { + final List requestCurrencies = auctionContext.getBidRequest().getCur(); + final List bidAcceptableCurrencies = + Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) + .map(BidderInfo::getCurrencyAccepted) + .orElse(null); + + if (CollectionUtils.isEmpty(requestCurrencies) || CollectionUtils.isEmpty(bidAcceptableCurrencies)) { + return false; + } + + return !CollectionUtils.containsAny(requestCurrencies, bidAcceptableCurrencies); + } + + private static Future processReject(AuctionContext auctionContext, + BidRejectionReason bidRejectionReason, + List warnings, + String bidderName) { + + auctionContext.getBidRejectionTrackers() + .get(bidderName) + .rejectAll(bidRejectionReason); + final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() + .warnings(warnings) + .build(); + return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); + } + private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { return CollectionUtils.isNotEmpty(warnings) ? seatBid.toBuilder() @@ -1416,13 +1372,6 @@ private List validateAndAdjustBids(List - * Removes invalid bids from response and adds corresponding error to {@link BidderSeatBid}. - *

- * Returns input argument as the result if no errors found or creates new {@link BidderResponse} otherwise. - */ private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, AuctionContext auctionContext, BidderAliases aliases) { @@ -1483,13 +1432,6 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); } - /** - * Performs changes on {@link Bid}s price depends on different between adServerCurrency and bidCurrency, - * and adjustment factor. Will drop bid if currency conversion is needed but not possible. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, BidRequest bidRequest) { if (auctionParticipation.isRequestBlocked()) { @@ -1594,13 +1536,6 @@ private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } - /** - * Updates 'request_time', 'responseTime', 'timeout_request', 'error_requests', 'no_bid_requests', - * 'prices' metrics for each {@link AuctionParticipation}. - *

- * This method should always be invoked after {@link ExchangeService#validBidderResponse} to make sure - * {@link Bid#getPrice()} is not empty. - */ private List updateResponsesMetrics(List auctionParticipations, Account account, BidderAliases aliases) { @@ -1649,9 +1584,6 @@ private Future invokeResponseHooks(AuctionContext auctionContext .map(auctionContext::with); } - /** - * Resolves {@link MetricName} by {@link BidderError.Type} value. - */ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { return switch (errorType) { case bad_input -> MetricName.badinput; diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java index 70fd0244bc5..e916ee0b3a5 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -58,6 +58,11 @@ public enum BidRejectionReason { */ REQUEST_BLOCKED_PRIVACY(204), + /** + * If the bidder was not called due to a mismatch between the bidder’s currency and the request’s currency. + */ + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), + /** * The bidder is called, but its response is rejected. * Applied if any other RESPONSE_REJECTED reason is not recognized. diff --git a/src/main/java/org/prebid/server/bidder/BidderInfo.java b/src/main/java/org/prebid/server/bidder/BidderInfo.java index fffbb1bab5b..c9659135eb7 100644 --- a/src/main/java/org/prebid/server/bidder/BidderInfo.java +++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java @@ -28,6 +28,8 @@ public class BidderInfo { List vendors; + List currencyAccepted; + GdprInfo gdpr; boolean ccpaEnforced; @@ -49,6 +51,7 @@ public static BidderInfo create(boolean enabled, List doohMediaTypes, List supportedVendors, int vendorId, + List currencyAccepted, boolean ccpaEnforced, boolean modifyingVastXmlAllowed, CompressionType compressionType, @@ -66,6 +69,7 @@ public static BidderInfo create(boolean enabled, platformInfo(siteMediaTypes), platformInfo(doohMediaTypes)), supportedVendors, + currencyAccepted, new GdprInfo(vendorId), ccpaEnforced, modifyingVastXmlAllowed, diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java index c25236dd799..bffc274c35d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/MetaInfo.java @@ -24,6 +24,8 @@ public class MetaInfo { private List supportedVendors; + private List currencyAccepted; + @NotNull private Integer vendorId; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java index 342ce998592..cd7553bb34a 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java @@ -28,6 +28,7 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope metaInfo.getDoohMediaTypes(), metaInfo.getSupportedVendors(), metaInfo.getVendorId(), + metaInfo.getCurrencyAccepted(), configurationProperties.getPbsEnforcesCcpa(), configurationProperties.getModifyingVastXmlAllowed(), configurationProperties.getEndpointCompression(), diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index b52c7ac21b1..2c15fd531dd 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -114,9 +114,9 @@ adapters: - video - native site-media-types: - - banner - - video - - native + - banner + - video + - native supported-vendors: vendor-id: 263 usersync: diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy index 3f14bac3db1..79cf8ad9317 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy @@ -14,6 +14,7 @@ enum BidRejectionReason { REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), REQUEST_BLOCKED_PRIVACY(204), + REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY(205), RESPONSE_REJECTED_GENERAL(300), RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR(301), diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy index 8b087120db0..cab50bd816b 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredImp @@ -16,19 +17,20 @@ import org.prebid.server.functional.model.request.auction.ImpExtContext import org.prebid.server.functional.model.request.auction.ImpExtContextData import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidStoredRequest -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.vtrack.VtrackRequest import org.prebid.server.functional.model.request.vtrack.xml.Vast import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.CcpaConsent +import static org.prebid.server.functional.model.Currency.CHF +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.CompressionType.GZIP import static org.prebid.server.functional.model.bidder.CompressionType.NONE import static org.prebid.server.functional.model.request.auction.Asset.titleAsset @@ -37,11 +39,15 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.SecurityLevel.NON_SECURE import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY +import static org.prebid.server.functional.model.response.auction.ErrorType.ALIAS +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.HttpUtil.CONTENT_ENCODING_HEADER import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED @@ -58,7 +64,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain httpcalls" - assert response.ext?.debug?.httpcalls[GENERIC.value] + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] and: "Response should not contain error" assert !response.ext?.errors @@ -84,7 +90,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [2] + assert response.ext?.errors[GENERIC]*.code == [2] where: adapterDefault | generic | adapterConfig @@ -212,7 +218,7 @@ class BidderParamsSpec extends BaseSpec { bidRequest.imp.first().ext.prebid.bidder.generic = new Generic(firstParam: firstParam) and: "Set bidderParam to bidRequest" - bidRequest.ext.prebid.bidderParams = [(GENERIC): [firstParam: PBSUtils.randomNumber]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [firstParam: PBSUtils.randomNumber]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -247,7 +253,7 @@ class BidderParamsSpec extends BaseSpec { and: "Set bidderParam to bidRequest" def secondParam = PBSUtils.randomNumber - bidRequest.ext.prebid.bidderParams = [(GENERIC): [secondParam: secondParam]] + bidRequest.ext.prebid.bidderParams = [(BidderName.GENERIC): [secondParam: secondParam]] when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -289,8 +295,8 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain error" - assert response.ext?.errors[ErrorType.GENERIC]*.code == [999] - assert response.ext?.errors[ErrorType.GENERIC]*.message == ["host name must not be empty"] + assert response.ext?.errors[GENERIC]*.code == [999] + assert response.ext?.errors[GENERIC]*.message == ["host name must not be empty"] } def "PBS should reject bidder when bidder params from request doesn't satisfy json-schema for auction request"() { @@ -395,8 +401,8 @@ class BidderParamsSpec extends BaseSpec { assert response.seatbid.isEmpty() and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == ["Bidder does not support any media types."] + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Bidder does not support any media types."] where: configMediaType | bidRequest @@ -512,8 +518,8 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequest.imp[0].nativeObj and: "Response should contain error" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from the " + "request for this bidder." as String] @@ -531,7 +537,7 @@ class BidderParamsSpec extends BaseSpec { def bidResponse = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert bidResponse.ext?.warnings[ErrorType.GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") + assert bidResponse.ext?.warnings[GENERIC]?.message.contains("Bid request contains 0 impressions after filtering.") and: "Bid response shouldn't contain any seatbid" assert !bidResponse.seatbid @@ -565,7 +571,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain proper warning" - assert response.ext?.warnings[ErrorType.GENERIC]?.message == + assert response.ext?.warnings[GENERIC]?.message == ["Imp ${bidRequest.imp[1].id} does not have a supported media type and has been removed from the request for this bidder."] and: "Bid response should contain seatbid" @@ -600,8 +606,8 @@ class BidderParamsSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 0 and: "Response should contain errors" - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [2, 2] - assert response.ext?.warnings[ErrorType.GENERIC]*.message == + assert response.ext?.warnings[GENERIC]*.code == [2, 2] + assert response.ext?.warnings[GENERIC]*.message == ["Imp ${bidRequest.imp[0].id} does not have a supported media type and has been removed from " + "the request for this bidder.", "Bid request contains 0 impressions after filtering."] @@ -646,7 +652,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain header Content-Encoding = gzip" - assert response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER)?.first() == compressionType } @@ -662,7 +668,7 @@ class BidderParamsSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Bidder request should not contain header Content-Encoding" - assert !response.ext?.debug?.httpcalls?.get(GENERIC.value)?.requestHeaders?.first() + assert !response.ext?.debug?.httpcalls?.get(BidderName.GENERIC.value)?.requestHeaders?.first() ?.get(CONTENT_ENCODING_HEADER) } @@ -805,4 +811,233 @@ class BidderParamsSpec extends BaseSpec { tid == impExt.tid } } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "") + + and: "Default bid request with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "") + + and: "Default bid request with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid" + assert !response.ext.seatnonbid + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[BidderName.GENERIC.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService("adapters.generic.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + and: "PBS should emit an warnings" + assert response.ext?.warnings[GENERIC]*.code == [999] + assert response.ext?.warnings[GENERIC]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.GENERIC.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${USD},${EUR}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain http calls" + assert response.ext?.debug?.httpcalls[ALIAS.value] + + and: "Response should contain seatBid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "PBS response shouldn't contain seatNonBid and contain errors" + assert !response.ext.seatnonbid + } + + def "PBS shouldn't send request to bidder and emit warning when adapters.bidder.aliases.bidder.meta-info.currency-accepted not intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsService = pbsServiceFactory.getService( + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": "${JPY},${CHF}".toString()) + + and: "Default basic BidRequest with alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [USD] + ext.prebid.returnAllBidStatus = true + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.generic = null + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain http calls" + assert !response.ext?.debug?.httpcalls + + and: "Response shouldn't contain seatBid" + assert !response.seatbid + + and: "Pbs shouldn't make bidder request" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "PBS should emit an warnings" + assert response.ext?.warnings[ALIAS]*.code == [999] + assert response.ext?.warnings[ALIAS]*.message == + ["No match between the configured currencies and bidRequest.cur"] + + and: "Response should seatNon bid with code 205" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == BidderName.ALIAS.value + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index ffff9684fb1..0c7710dbc74 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -48,6 +48,8 @@ import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.model.BidderRequest; @@ -329,6 +331,7 @@ public void setUp() { null, null, 0, + null, false, false, CompressionType.NONE, @@ -4668,6 +4671,62 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportProvidedMediaTypes() .isEqualTo(BidResponse.builder().id("uniqId").build()); } + @Test + public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() { + // given + final Imp imp = givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")); + final BidRequest bidRequest = givenBidRequest(singletonList(imp), + bidRequestBuilder -> bidRequestBuilder.cur(singletonList("USD"))); + final AuctionContext auctionContext = givenRequestContext(bidRequest); + + given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create( + true, + null, + false, + null, + null, + null, + null, + null, + null, + null, + 0, + singletonList("CAD"), + false, + false, + CompressionType.NONE, + Ortb.of(false))); + given(bidResponseCreator.create( + argThat(argument -> argument.getAuctionParticipations().getFirst() + .getBidderResponse() + .equals(BidderResponse.of( + "bidder1", + BidderSeatBid.builder() + .warnings(Collections.singletonList( + BidderError.generic( + "No match between the configured currencies and bidRequest.cur" + ))) + .build(), + 0))), + any(), + any())) + .willReturn(Future.succeededFuture(BidResponse.builder().id("uniqId").build())); + + // when + final Future result = target.holdAuction(auctionContext); + + // then + assertThat(result.result()) + .extracting(AuctionContext::getBidResponse) + .isEqualTo(BidResponse.builder().id("uniqId").build()); + assertThat(result.result()) + .extracting(AuctionContext::getBidRejectionTrackers) + .extracting(rejectionTrackers -> rejectionTrackers.get("bidder1")) + .extracting(BidRejectionTracker::getRejectionReasons) + .isEqualTo(Map.of("impId1", BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY)); + + } + @Test public void shouldConvertBidRequestOpenRTBVersionToConfiguredByBidder() { // given diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java index 8103416ef22..60ff60318e1 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java @@ -171,6 +171,7 @@ private static BidderInfo givenBidderInfo(List appMediaTypes, doohMediaType, emptyList(), 0, + null, false, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java index 2e0e0422450..e2394769585 100644 --- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java @@ -275,6 +275,7 @@ private static BidderInfo givenBidderInfo(boolean multiFormatSupported) { emptyList(), emptyList(), 0, + emptyList(), false, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java index b8287e6c086..5459e232386 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java @@ -76,6 +76,7 @@ public void setUp() { null, null, 0, + null, true, false, null, @@ -213,6 +214,7 @@ public void enforceShouldSkipNoSaleBiddersAndNotEnforcedByBidderConfig() { null, null, 0, + null, false, false, null, diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java index 6fbb3546a02..f2e0207fc4a 100644 --- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java +++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java @@ -95,6 +95,7 @@ public void metaInfoByNameShouldReturnMetaInfoForKnownBidderIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -127,6 +128,7 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -150,6 +152,7 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -186,6 +189,7 @@ public void resolveBaseBidderShouldReturnBaseBidderName() { emptyList(), null, 0, + null, true, false, CompressionType.NONE, @@ -252,6 +256,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -269,6 +274,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -286,6 +292,7 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, @@ -354,6 +361,7 @@ public void nameByVendorIdShouldReturnBidderNameForVendorId() { singletonList(MediaType.AUDIO), null, 99, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java index 440ba94cd2f..38cad500e7b 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java @@ -170,6 +170,7 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderConfig() { null, null, 0, + null, false, false, CompressionType.GZIP, @@ -207,6 +208,7 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderAliasConfig() { null, null, 0, + null, false, false, CompressionType.GZIP, diff --git a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java index e78e65fa082..d8589113f31 100644 --- a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java @@ -195,6 +195,7 @@ private static BidderInfo givenBidderInfo(boolean enabled, String endpoint, Stri singletonList(MediaType.NATIVE), null, 0, + null, true, false, CompressionType.NONE, diff --git a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java index 863ce3bb53b..a14c6141355 100644 --- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java @@ -394,6 +394,7 @@ private static BidderInfo givenBidderInfo(String aliasOf) { null, null, 0, + null, true, false, CompressionType.NONE, From 0c569331c86fb57012a7b995f12fa12bb6452548 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:01:00 +0300 Subject: [PATCH 045/170] PBJ: Not stable Price Floors test fix (#3426) --- .../org/prebid/server/it/PriceFloorsTest.java | 75 ++++++++++++++++--- .../prebid/server/it/test-app-settings.yaml | 2 +- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java index e9ff0df5dd3..b32be1580d2 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -1,12 +1,18 @@ package org.prebid.server.it; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.prebid.server.model.Endpoint; import org.prebid.server.util.IntegrationTestsUtil; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; import java.io.IOException; @@ -16,14 +22,40 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; - -@TestPropertySource(properties = {"price-floors.enabled=true", "server.http.port=8050", "admin.port=0"}) -public class PriceFloorsTest extends IntegrationTest { +import static org.prebid.server.util.IntegrationTestsUtil.assertJsonEquals; +import static org.prebid.server.util.IntegrationTestsUtil.jsonFrom; +import static org.prebid.server.util.IntegrationTestsUtil.responseFor; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@TestPropertySource( + locations = {"test-application.properties"}, + properties = { + "price-floors.enabled=true", + "server.http.port=8050", + "admin.port=0", + "settings.in-memory-cache.http-update.endpoint=http://localhost:8100/periodic-update", + "settings.in-memory-cache.http-update.amp-endpoint=http://localhost:8100/periodic-update", + "currency-converter.external-rates.url=http://localhost:8100/currency-rates", + "adapters.generic.endpoint=http://localhost:8100/generic-exchange" + }) +public class PriceFloorsTest { private static final int APP_PORT = 8050; + private static final int WIREMOCK_PORT = 8100; + + @SuppressWarnings("unchecked") + @RegisterExtension + public static final WireMockExtension WIRE_MOCK_RULE = WireMockExtension.newInstance() + .options(wireMockConfig() + .port(WIREMOCK_PORT) + .gzipDisabled(true) + .jettyStopTimeout(5000L) + .extensions(IntegrationTest.CacheResponseTransformer.class)) + .build(); private static final String PRICE_FLOORS = "Price Floors Test"; private static final String FLOORS_FROM_REQUEST = "Floors from request"; @@ -31,6 +63,24 @@ public class PriceFloorsTest extends IntegrationTest { private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + @BeforeAll + public static void beforeAll() throws IOException { + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/periodic-update")) + .willReturn(aResponse().withBody(jsonFrom("storedrequests/test-periodic-refresh.json")))); + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/currency-rates")) + .willReturn(aResponse().withBody(jsonFrom("currency/latest.json")))); + } + + @BeforeEach + public void setUp() throws IOException { + beforeAll(); + } + + @AfterEach + public void resetWireMock() { + WIRE_MOCK_RULE.resetAll(); + } + @Test public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IOException, JSONException { // given @@ -45,7 +95,7 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO .willSetStateTo(FLOORS_FROM_REQUEST)); // when - final Response firstResponse = IntegrationTestsUtil.responseFor( + final Response firstResponse = responseFor( "openrtb2/floors/floors-test-auction-request-1.json", Endpoint.openrtb2_auction, SPEC); @@ -54,7 +104,8 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", firstResponse, - singletonList("generic")); + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); // given final StubMapping stubMapping = WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) @@ -65,7 +116,7 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO .willSetStateTo(FLOORS_FROM_PROVIDER)); // when - final Response secondResponse = IntegrationTestsUtil.responseFor( + final Response secondResponse = responseFor( "openrtb2/floors/floors-test-auction-request-2.json", Endpoint.openrtb2_auction, SPEC); @@ -75,7 +126,8 @@ public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IO assertJsonEquals( "openrtb2/floors/floors-test-auction-response.json", secondResponse, - singletonList("generic")); + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); } @Test @@ -87,7 +139,7 @@ public void openrtb2AuctionShouldSkipPriceFloorsForTheGenericBidderWhenGenericIs .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/floors-test-bid-response.json")))); // when - final Response firstResponse = IntegrationTestsUtil.responseFor( + final Response firstResponse = responseFor( "openrtb2/floors/floors-test-auction-request-no-signal.json", Endpoint.openrtb2_auction, SPEC); @@ -96,6 +148,11 @@ public void openrtb2AuctionShouldSkipPriceFloorsForTheGenericBidderWhenGenericIs assertJsonEquals( "openrtb2/floors/floors-test-auction-response-no-signal.json", firstResponse, - singletonList("generic")); + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); + } + + private static String replaceBidderRelatedStaticInfo(String json, String bidder) { + return IntegrationTestsUtil.replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT); } } diff --git a/src/test/resources/org/prebid/server/it/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml index ef28a3481be..786b376ffed 100644 --- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml +++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml @@ -124,7 +124,7 @@ accounts: auction: price-floors: fetch: - url: http://localhost:8090/floors-provider + url: http://localhost:8100/floors-provider enabled: true domains: - rubiconproject.com From 65960a5d522a51d139f1cb7892baf469692d1f2c Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Thu, 5 Sep 2024 15:01:37 +0200 Subject: [PATCH 046/170] Rubicon: Remove unnecessary property (#3422) --- src/main/resources/bidder-config/rubicon.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/bidder-config/rubicon.yaml b/src/main/resources/bidder-config/rubicon.yaml index 38a3e2d24aa..882732f83a5 100644 --- a/src/main/resources/bidder-config/rubicon.yaml +++ b/src/main/resources/bidder-config/rubicon.yaml @@ -38,7 +38,6 @@ adapters: url: GET_FROM_globalsupport@magnite.com support-cors: false generate-bid-id: false - use-video-size-id-logic: true XAPI: Username: GET_FROM_globalsupport@magnite.com Password: GET_FROM_globalsupport@magnite.com From 01cc32c8f2094d94cbad4512888a2795bae53890 Mon Sep 17 00:00:00 2001 From: bretg Date: Mon, 9 Sep 2024 07:09:05 -0400 Subject: [PATCH 047/170] Docs: Update code-reviews.md (#3431) --- docs/developers/code-reviews.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md index 78728fef18a..cc8ed667849 100644 --- a/docs/developers/code-reviews.md +++ b/docs/developers/code-reviews.md @@ -43,3 +43,4 @@ explaining it. Are there better ways to achieve those goals? - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead! - Can the code be organized into smaller, more modular pieces? - Is there dead code which can be deleted? Or TODO comments which should be resolved? +- Look for code used by other adapters. Encourage adapter submitter to utilize common code. From 509d2a4c074219598aff8166d0ec4013c8ec917f Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Mon, 9 Sep 2024 13:09:46 +0200 Subject: [PATCH 048/170] Oraki: Add new adapter (#3423) --- .../server/bidder/oraki/OrakiBidder.java | 138 ++++++++ .../bidder/oraki/proto/OrakiImpExtBidder.java | 18 + .../ext/request/oraki/ExtImpOraki.java | 14 + .../config/bidder/OrakiConfiguration.java | 41 +++ src/main/resources/bidder-config/oraki.yaml | 21 ++ .../resources/static/bidder-params/oraki.json | 30 ++ .../server/bidder/oraki/OrakiBidderTest.java | 327 ++++++++++++++++++ .../java/org/prebid/server/it/OrakiTest.java | 33 ++ .../oraki/test-auction-oraki-request.json | 26 ++ .../oraki/test-auction-oraki-response.json | 37 ++ .../oraki/test-oraki-bid-request.json | 59 ++++ .../oraki/test-oraki-bid-response.json | 20 ++ .../server/it/test-application.properties | 2 + 13 files changed, 766 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java create mode 100644 src/main/resources/bidder-config/oraki.yaml create mode 100644 src/main/resources/static/bidder-params/oraki.json create mode 100644 src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/OrakiTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java new file mode 100644 index 00000000000..b7e9ff64afa --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.oraki; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder; +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.oraki.ExtImpOraki; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class OrakiBidder implements Bidder { + + private static final TypeReference> ORAKI_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OrakiBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ExtImpOraki extImpOraki; + try { + extImpOraki = parseImpExt(imp); + outgoingRequests.add(createSingleRequest(modifyImp(imp, extImpOraki), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(outgoingRequests, errors); + } + + private ExtImpOraki parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ORAKI_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpOraki extImpOraki) { + final OrakiImpExtBidder orakiImpExtBidder = getImpExtOrakiWithType(extImpOraki); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(orakiImpExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private OrakiImpExtBidder getImpExtOrakiWithType(ExtImpOraki extImpOraki) { + final boolean hasPlacementId = StringUtils.isNotBlank(extImpOraki.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(extImpOraki.getEndpointId()); + + return OrakiImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? extImpOraki.getPlacementId() : null) + .endpointId(hasEndpointId ? extImpOraki.getEndpointId() : null) + .build(); + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} + diff --git a/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java b/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java new file mode 100644 index 00000000000..4c3e8c3b9f5 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/proto/OrakiImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.oraki.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class OrakiImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java new file mode 100644 index 00000000000..860ddb430d9 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/oraki/ExtImpOraki.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.oraki; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOraki { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java new file mode 100644 index 00000000000..8c5fbc6abe2 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OrakiConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.oraki.OrakiBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/oraki.yaml", factory = YamlPropertySourceFactory.class) +public class OrakiConfiguration { + + private static final String BIDDER_NAME = "oraki"; + + @Bean("orakiConfigurationProperties") + @ConfigurationProperties("adapters.oraki") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps orakiBidderDeps(BidderConfigurationProperties orakiConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(orakiConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OrakiBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/oraki.yaml b/src/main/resources/bidder-config/oraki.yaml new file mode 100644 index 00000000000..f5197ac83ff --- /dev/null +++ b/src/main/resources/bidder-config/oraki.yaml @@ -0,0 +1,21 @@ +adapters: + oraki: + endpoint: https://eu1.oraki.io/pserver + meta-info: + maintainer-email: prebid@oraki.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: oraki + redirect: + support-cors: false + url: https://sync.oraki.io/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/static/bidder-params/oraki.json b/src/main/resources/static/bidder-params/oraki.json new file mode 100644 index 00000000000..9a2d596eeff --- /dev/null +++ b/src/main/resources/static/bidder-params/oraki.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Oraki Adapter Params", + "description": "A schema which validates params accepted by the Oraki adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java b/src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java new file mode 100644 index 00000000000..987d71658c2 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/oraki/OrakiBidderTest.java @@ -0,0 +1,327 @@ +package org.prebid.server.bidder.oraki; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder; +import org.prebid.server.bidder.oraki.proto.OrakiImpExtBidder.OrakiImpExtBidderBuilder; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.oraki.ExtImpOraki; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class OrakiBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final OrakiBidder target = new OrakiBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new OrakiBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final Imp givenImp1 = givenImp(imp -> imp.id("givenImp1")); + final Imp givenImp2 = givenImp(imp -> imp.id("givenImp2")); + final BidRequest bidRequest = BidRequest.builder().imp(List.of(givenImp1, givenImp2)).build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final Imp givenInvalidImp = givenImp(imp -> imp + .id("impIdCorrupted") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final Imp givenValidImp = givenImp(identity()); + + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(givenInvalidImp, givenValidImp)) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("123"); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList(givenImp(identity()), givenImp(identity()))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOraki.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtOrakiBidder(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOraki.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExtOrakiBidder(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("123"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + + return BidRequest.builder() + .imp(singletonList(givenImp(impCustomizer))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("123") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpOraki.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExtOrakiBidder(UnaryOperator impExtOraki) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExtOraki.apply(OrakiImpExtBidder.builder()).build(), + JsonNode.class)); + } +} + diff --git a/src/test/java/org/prebid/server/it/OrakiTest.java b/src/test/java/org/prebid/server/it/OrakiTest.java new file mode 100644 index 00000000000..ff7b8880c32 --- /dev/null +++ b/src/test/java/org/prebid/server/it/OrakiTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class OrakiTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromOraki() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/oraki-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/oraki/test-oraki-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/oraki/test-oraki-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/oraki/test-auction-oraki-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/oraki/test-auction-oraki-response.json", response, + singletonList("oraki")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json new file mode 100644 index 00000000000..0b738e1b9d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "oraki": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json new file mode 100644 index 00000000000..6871f609875 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "oraki", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "oraki": "{{ oraki.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-oraki-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} 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 50562212bd7..cb54ca7386e 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -324,6 +324,8 @@ adapters.openx.enabled=true adapters.openx.endpoint=http://localhost:8090/openx-exchange adapters.operaads.enabled=true adapters.operaads.endpoint=http://localhost:8090/operaads-exchange +adapters.oraki.enabled=true +adapters.oraki.endpoint=http://localhost:8090/oraki-exchange adapters.orbidder.enabled=true adapters.orbidder.endpoint=http://localhost:8090/orbidder-exchange adapters.outbrain.enabled=true From 1e02fc4604989b23dc3d82e30ac812681f43e39a Mon Sep 17 00:00:00 2001 From: Piotr Jaworski <109736938+piotrj-rtbh@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:23:15 +0200 Subject: [PATCH 049/170] RTB House: Resolve oRTB `AUCTION_PRICE` macro (#3421) --- .../bidder/rtbhouse/RtbhouseBidder.java | 13 ++++++++++- .../bidder/rtbhouse/RtbhouseBidderTest.java | 22 +++++++++++++++++++ .../test-auction-rtbhouse-response.json | 2 +- .../rtbhouse/test-rtbhouse-bid-response.json | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java index 91a8b5b09c4..39c269b163b 100644 --- a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java +++ b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java @@ -41,6 +41,7 @@ public class RtbhouseBidder implements Bidder { new TypeReference<>() { }; private static final String BIDDER_CURRENCY = "USD"; + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; private final String endpointUrl; private final JacksonMapper mapper; @@ -127,7 +128,7 @@ private BidderBid resolveBidderBid(Bid bid, .build(); return BidderBid.builder() - .bid(updatedBid) + .bid(resolveMacros(updatedBid)) .type(bidType) .bidCurrency(currency) .build(); @@ -212,4 +213,14 @@ private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidR } } + private static Bid resolveMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + return bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .build(); + } + } diff --git a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java index a31a943c0e2..ed4731a5320 100644 --- a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java @@ -301,6 +301,28 @@ public void makeBidsShouldParseNativeAdmData() throws JsonProcessingException { .containsExactly("{\"property1\":\"value1\"}"); } + @Test + public void makeBidsShouldReturnBidWithResolvedMacros() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(null)); + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(givenBidResponse( + bidBuilder -> bidBuilder + .nurl("nurl:${AUCTION_PRICE}") + .adm("adm:${AUCTION_PRICE}") + .price(BigDecimal.valueOf(12.34))))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getNurl, Bid::getAdm) + .containsExactly(tuple("nurl:12.34", "adm:12.34")); + } + private static BidResponse givenBidResponse(Function bidCustomizer) { return BidResponse.builder() .cur("USD") diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json index c137b840e4b..841d82b403c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json @@ -7,7 +7,7 @@ "id": "bid_id", "impid": "imp_id", "price": 0.5, - "adm": "some-test-ad", + "adm": "some-test-ad_0.5", "adid": "12345678", "cid": "987", "crid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json index 94da12115c9..9b33d159d6f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-rtbhouse-bid-response.json @@ -9,7 +9,7 @@ "impid": "imp_id", "price": 0.500000, "adid": "12345678", - "adm": "some-test-ad", + "adm": "some-test-ad_${AUCTION_PRICE}", "cid": "987", "crid": "12345678", "h": 250, From 4fac10cfd056f921595d5f9b54a98dc7ecff1d0f Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:28:39 +0300 Subject: [PATCH 050/170] Core: S3 application settings (#3418) --- docs/application-settings.md | 45 ++ ...iantAdQualityBidResponsesScanHookTest.java | 6 +- .../v1/ConfiantAdQualityModuleTest.java | 6 +- ...iceDetectionRawAuctionRequestHookTest.java | 7 - extra/pom.xml | 6 + pom.xml | 9 + sample/configs/prebid-config-s3.yaml | 60 +++ .../settings/S3ApplicationSettings.java | 227 ++++++++++ .../service/S3PeriodicRefreshService.java | 146 +++++++ .../spring/config/SettingsConfiguration.java | 143 ++++++- .../functional/service/S3Service.groovy | 103 +++++ .../testcontainers/Dependencies.groovy | 10 +- .../tests/storage/AccountS3Spec.groovy | 118 +++++ .../functional/tests/storage/AmpS3Spec.groovy | 115 +++++ .../tests/storage/AuctionS3Spec.groovy | 117 +++++ .../tests/storage/StorageBaseSpec.groovy | 56 +++ .../tests/storage/StoredResponseS3Spec.groovy | 99 +++++ .../auction/BidResponseCreatorTest.java | 2 - .../bidder/algorix/AlgorixBidderTest.java | 1 - .../mobilefuse/MobilefuseBidderTest.java | 1 - .../bidder/rtbhouse/RtbhouseBidderTest.java | 2 - .../bidder/smaato/SmaatoBidderTest.java | 1 - .../server/cache/CoreCacheServiceTest.java | 1 - .../floors/BasicPriceFloorProcessorTest.java | 2 - .../handler/openrtb2/AmpHandlerTest.java | 1 - .../handler/openrtb2/AuctionHandlerTest.java | 1 - .../org/prebid/server/it/AdnuntiusTest.java | 1 - .../org/prebid/server/it/MinuteMediaTest.java | 1 - .../org/prebid/server/it/PrecisoTest.java | 2 +- .../CachingApplicationSettingsTest.java | 1 - .../settings/S3ApplicationSettingsTest.java | 403 ++++++++++++++++++ .../service/S3PeriodicRefreshServiceTest.java | 174 ++++++++ 32 files changed, 1821 insertions(+), 46 deletions(-) create mode 100644 sample/configs/prebid-config-s3.yaml create mode 100644 src/main/java/org/prebid/server/settings/S3ApplicationSettings.java create mode 100644 src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java create mode 100644 src/test/groovy/org/prebid/server/functional/service/S3Service.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy create mode 100644 src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java create mode 100644 src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java diff --git a/docs/application-settings.md b/docs/application-settings.md index c51febaea3e..39fd52c7e5a 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -259,6 +259,51 @@ Here's an example YAML file containing account-specific settings: default: true ``` +## Setting Account Configuration in S3 + +This is identical to the account configuration in a file system, with the main difference that your file system is +[AWS S3](https://aws.amazon.com/de/s3/) or any S3 compatible storage, such as [MinIO](https://min.io/). + + +The general idea is that you'll place all the account-specific settings in a separate YAML file and point to that file. + +```yaml +settings: + s3: + accessKeyId: + secretAccessKey: + endpoint: # http://s3.storage.com + bucket: # prebid-application-settings + region: # if not provided AWS_GLOBAL will be used. Example value: 'eu-central-1' + accounts-dir: accounts + stored-imps-dir: stored-impressions + stored-requests-dir: stored-requests + stored-responses-dir: stored-responses + + # recommended to configure an in memory cache, but this is optional + in-memory-cache: + # example settings, tailor to your needs + cache-size: 100000 + ttl-seconds: 1200 # 20 minutes + # recommended to configure + s3-update: + refresh-rate: 900000 # Refresh every 15 minutes + timeout: 5000 +``` + +### File format + +We recommend using the `json` format for your account configuration. A minimal configuration may look like this. + +```json +{ + "id" : "979c7116-1f5a-43d4-9a87-5da3ccc4f52c", + "status" : "active" +} +``` + +This pairs nicely if you have a default configuration defined in your prebid server config under `settings.default-account-config`. + ## Setting Account Configuration in the Database In database approach account properties are stored in database table(s). diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java index 2bd6a01b993..d4b39a8214e 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java @@ -71,11 +71,7 @@ public void setUp() { @Test public void codeShouldHaveValidConfigsWhenInitialized() { - // given - - // when - - // then + // when and then assertThat(target.code()).isEqualTo("confiant-ad-quality-bid-responses-scan-hook"); } diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java index 33ad4eef240..41e63920319 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java @@ -8,11 +8,7 @@ public class ConfiantAdQualityModuleTest { @Test public void shouldHaveValidInitialConfigs() { - // given - - // when - - // then + // when and then assertThat(ConfiantAdQualityModule.CODE).isEqualTo("confiant-ad-quality"); } } diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java index 5e3582b5297..cfd079299a0 100644 --- a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHookTest.java @@ -402,7 +402,6 @@ public void callShouldReturnUpdateActionWhenFilterIsNull() { @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAuctionContext() { // given - final AuctionInvocationContext context = AuctionInvocationContextImpl.of( null, null, @@ -470,7 +469,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAuctionContext @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccount() { // given - final AuctionContext auctionContext = AuctionContext.builder().build(); final AuctionInvocationContext context = AuctionInvocationContextImpl.of( null, @@ -493,7 +491,6 @@ public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccount() { @Test public void callShouldReturnNoUpdateActionWhenNoWhitelistAndNoAccountButDeviceIdIsSet() { // given - final AuctionContext auctionContext = AuctionContext.builder().build(); final AuctionInvocationContext context = AuctionInvocationContextImpl.of( null, @@ -568,7 +565,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAccount() { @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNoAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .build()) @@ -648,7 +644,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndNoAccountID() { @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndEmptyAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .id("") @@ -731,7 +726,6 @@ public void callShouldReturnNoUpdateActionWhenWhitelistFilledAndEmptyAccountID() @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndAllowedAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .id("42") @@ -814,7 +808,6 @@ public void callShouldReturnUpdateActionWhenWhitelistFilledAndAllowedAccountID() @Test public void callShouldReturnUpdateActionWhenNoWhitelistAndNotAllowedAccountID() { // given - final AuctionContext auctionContext = AuctionContext.builder() .account(Account.builder() .id("29") diff --git a/extra/pom.xml b/extra/pom.xml index b3ec8b88bc8..3364584f818 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -52,6 +52,7 @@ 3.21.7 3.17.3 1.0.7 + 2.26.24 3.9.1 @@ -212,6 +213,11 @@ geoip2 ${maxmind-client.version} + + software.amazon.awssdk + s3 + ${aws.awssdk.version} + com.google.protobuf protobuf-java diff --git a/pom.xml b/pom.xml index f47f59fb1cf..f58a7184f2f 100644 --- a/pom.xml +++ b/pom.xml @@ -170,6 +170,10 @@ org.postgresql postgresql + + software.amazon.awssdk + s3 + com.github.ben-manes.caffeine caffeine @@ -328,6 +332,11 @@ mysql test + + org.testcontainers + localstack + test + org.testcontainers postgresql diff --git a/sample/configs/prebid-config-s3.yaml b/sample/configs/prebid-config-s3.yaml new file mode 100644 index 00000000000..277ad94613c --- /dev/null +++ b/sample/configs/prebid-config-s3.yaml @@ -0,0 +1,60 @@ +status-response: "ok" + +server: + enable-quickack: true + enable-reuseport: true + +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + s3: + accessKeyId: prebid-server-test + secretAccessKey: nq9h6whXQURNL2NnWg3rcMlLMtGGDJeWrdl8hC9g + endpoint: http://localhost:9000 + bucket: prebid-server-configs.example.com # prebid-application-settings + force-path-style: true # virtual bucketing + # region: # if not provided AWS_GLOBAL will be used. Example value: 'eu-central-1' + accounts-dir: accounts + stored-imps-dir: stored-impressions + stored-requests-dir: stored-requests + stored-responses-dir: stored-responses + + in-memory-cache: + cache-size: 10000 + ttl-seconds: 1200 # 20 minutes + s3-update: + refresh-rate: 900000 # Refresh every 15 minutes + timeout: 5000 + +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 + +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java new file mode 100644 index 00000000000..f6198a5ad94 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -0,0 +1,227 @@ +package org.prebid.server.settings; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; +import software.amazon.awssdk.core.BytesWrapper; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Implementation of {@link ApplicationSettings}. + *

+ * Reads an application settings from JSON file in a s3 bucket, stores and serves them in and from the memory. + *

+ * Immediately loads stored request data from local files. These are stored in memory for low-latency reads. + * This expects each file in the directory to be named "{config_id}.json". + */ +public class S3ApplicationSettings implements ApplicationSettings { + + private static final String JSON_SUFFIX = ".json"; + + final S3AsyncClient asyncClient; + final String bucket; + final String accountsDirectory; + final String storedImpressionsDirectory; + final String storedRequestsDirectory; + final String storedResponsesDirectory; + final JacksonMapper jacksonMapper; + final Vertx vertx; + + public S3ApplicationSettings(S3AsyncClient asyncClient, + String bucket, + String accountsDirectory, + String storedImpressionsDirectory, + String storedRequestsDirectory, + String storedResponsesDirectory, + JacksonMapper jacksonMapper, + Vertx vertx) { + + this.asyncClient = Objects.requireNonNull(asyncClient); + this.bucket = Objects.requireNonNull(bucket); + this.accountsDirectory = Objects.requireNonNull(accountsDirectory); + this.storedImpressionsDirectory = Objects.requireNonNull(storedImpressionsDirectory); + this.storedRequestsDirectory = Objects.requireNonNull(storedRequestsDirectory); + this.storedResponsesDirectory = Objects.requireNonNull(storedResponsesDirectory); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.vertx = Objects.requireNonNull(vertx); + } + + @Override + public Future getAccountById(String accountId, Timeout timeout) { + return withTimeout(() -> downloadFile(accountsDirectory + "/" + accountId + JSON_SUFFIX), timeout) + .map(fileContent -> decodeAccount(fileContent, accountId)); + } + + private Account decodeAccount(String fileContent, String requestedAccountId) { + if (fileContent == null) { + throw new PreBidException("Account with id %s not found".formatted(requestedAccountId)); + } + + final Account account; + try { + account = jacksonMapper.decodeValue(fileContent, Account.class); + } catch (DecodeException e) { + throw new PreBidException("Invalid json for account with id %s".formatted(requestedAccountId)); + } + + validateAccount(account, requestedAccountId); + return account; + } + + private static void validateAccount(Account account, String requestedAccountId) { + final String receivedAccountId = account != null ? account.getId() : null; + if (!StringUtils.equals(receivedAccountId, requestedAccountId)) { + throw new PreBidException( + "Account with id %s does not match id %s in file".formatted(requestedAccountId, receivedAccountId)); + } + } + + @Override + public Future getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return withTimeout( + () -> Future.all( + getFileContents(storedRequestsDirectory, requestIds), + getFileContents(storedImpressionsDirectory, impIds)), + timeout) + .map(results -> buildStoredDataResult( + results.resultAt(0), + results.resultAt(1), + requestIds, + impIds)); + } + + private StoredDataResult buildStoredDataResult(Map storedIdToRequest, + Map storedIdToImp, + Set requestIds, + Set impIds) { + + final List errors = Stream.concat( + missingStoredDataIds(storedIdToImp, impIds).stream() + .map("No stored impression found for id: %s"::formatted), + missingStoredDataIds(storedIdToRequest, requestIds).stream() + .map("No stored request found for id: %s"::formatted)) + .toList(); + + return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors); + } + + private Set missingStoredDataIds(Map fileContents, Set responseIds) { + return SetUtils.difference(responseIds, fileContents.keySet()); + } + + @Override + public Future getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + } + + @Override + public Future getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return withTimeout(() -> getFileContents(storedResponsesDirectory, responseIds), timeout) + .map(storedIdToResponse -> StoredResponseDataResult.of( + storedIdToResponse, + missingStoredDataIds(storedIdToResponse, responseIds).stream() + .map("No stored response found for id: %s"::formatted) + .toList())); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return Future.succeededFuture(Collections.emptyMap()); + } + + private Future> getFileContents(String directory, Set ids) { + return Future.join(ids.stream() + .map(impId -> downloadFile(directory + withInitialSlash(impId) + JSON_SUFFIX) + .map(fileContent -> Tuple2.of(impId, fileContent))) + .toList()) + .map(CompositeFuture::>list) + .map(impIdToFileContent -> impIdToFileContent.stream() + .filter(tuple -> tuple.getRight() != null) + .collect(Collectors.toMap(Tuple2::getLeft, Tuple2::getRight))); + } + + /** + * When the impression id is the ad unit path it may already start with a slash and there's no need to add + * another one. + * + * @param impressionId from the bid request + * @return impression id with only a single slash at the beginning + */ + private static String withInitialSlash(String impressionId) { + return impressionId.startsWith("/") ? impressionId : "/" + impressionId; + } + + private Future downloadFile(String key) { + final GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + return Future.fromCompletionStage( + asyncClient.getObject(request, AsyncResponseTransformer.toBytes()), + vertx.getOrCreateContext()) + .map(BytesWrapper::asUtf8String) + .otherwiseEmpty(); + } + + private Future withTimeout(Supplier> futureFactory, Timeout timeout) { + final long remainingTime = timeout.remaining(); + if (remainingTime <= 0L) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final Promise promise = Promise.promise(); + final Future future = futureFactory.get(); + + final long timerId = vertx.setTimer(remainingTime, id -> + promise.tryFail(new TimeoutException("Timeout has been exceeded"))); + + future.onComplete(result -> { + vertx.cancelTimer(timerId); + if (result.succeeded()) { + promise.tryComplete(result.result()); + } else { + promise.tryFail(result.cause()); + } + }); + + return promise.future(); + } +} diff --git a/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java new file mode 100644 index 00000000000..d5a8ce7f873 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/service/S3PeriodicRefreshService.java @@ -0,0 +1,146 @@ +package org.prebid.server.settings.service; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.CacheNotificationListener; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.vertx.Initializable; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.S3Object; + +import java.time.Clock; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * Service that periodically calls s3 for stored request updates. + * If refreshRate is negative, then the data will never be refreshed. + *

+ * Fetches all files from the specified folders/prefixes in s3 and downloads all files. + */ +public class S3PeriodicRefreshService implements Initializable { + + private static final String JSON_SUFFIX = ".json"; + + private static final Logger logger = LoggerFactory.getLogger(S3PeriodicRefreshService.class); + + private final S3AsyncClient asyncClient; + private final String bucket; + private final String storedRequestsDirectory; + private final String storedImpressionsDirectory; + private final long refreshPeriod; + private final CacheNotificationListener cacheNotificationListener; + private final MetricName cacheType; + private final Clock clock; + private final Metrics metrics; + private final Vertx vertx; + + public S3PeriodicRefreshService(S3AsyncClient asyncClient, + String bucket, + String storedRequestsDirectory, + String storedImpressionsDirectory, + long refreshPeriod, + CacheNotificationListener cacheNotificationListener, + MetricName cacheType, + Clock clock, + Metrics metrics, + Vertx vertx) { + + this.asyncClient = Objects.requireNonNull(asyncClient); + this.bucket = Objects.requireNonNull(bucket); + this.storedRequestsDirectory = Objects.requireNonNull(storedRequestsDirectory); + this.storedImpressionsDirectory = Objects.requireNonNull(storedImpressionsDirectory); + this.refreshPeriod = refreshPeriod; + this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener); + this.cacheType = Objects.requireNonNull(cacheType); + this.clock = Objects.requireNonNull(clock); + this.metrics = Objects.requireNonNull(metrics); + this.vertx = Objects.requireNonNull(vertx); + } + + @Override + public void initialize(Promise initializePromise) { + fetchStoredDataResult(clock.millis(), MetricName.initialize) + .mapEmpty() + .onComplete(initializePromise); + + if (refreshPeriod > 0) { + logger.info("Starting s3 periodic refresh for " + cacheType + " every " + refreshPeriod + " s"); + vertx.setPeriodic(refreshPeriod, ignored -> fetchStoredDataResult(clock.millis(), MetricName.update)); + } + } + + private Future fetchStoredDataResult(long startTime, MetricName metricName) { + return Future.all( + getFileContentsForDirectory(storedRequestsDirectory), + getFileContentsForDirectory(storedImpressionsDirectory)) + .map(CompositeFuture::>list) + .map(results -> StoredDataResult.of(results.getFirst(), results.get(1), Collections.emptyList())) + .onSuccess(storedDataResult -> handleResult(storedDataResult, startTime, metricName)) + .onFailure(exception -> handleFailure(exception, startTime, metricName)); + } + + private Future> getFileContentsForDirectory(String directory) { + return listFiles(directory) + .map(files -> files.stream().map(this::downloadFile).toList()) + .compose(Future::all) + .map(CompositeFuture::>list) + .map(fileNameToContent -> fileNameToContent.stream() + .collect(Collectors.toMap( + entry -> stripFileName(directory, entry.getLeft()), + Tuple2::getRight))); + } + + private Future> listFiles(String prefix) { + final ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder() + .bucket(bucket) + .prefix(prefix) + .build(); + + return Future.fromCompletionStage(asyncClient.listObjects(listObjectsRequest), vertx.getOrCreateContext()) + .map(response -> response.contents().stream() + .map(S3Object::key) + .collect(Collectors.toList())); + } + + private Future> downloadFile(String key) { + final GetObjectRequest request = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + return Future.fromCompletionStage( + asyncClient.getObject(request, AsyncResponseTransformer.toBytes()), + vertx.getOrCreateContext()) + .map(content -> Tuple2.of(key, content.asUtf8String())); + } + + private static String stripFileName(String directory, String name) { + return name + .replace(directory + "/", "") + .replace(JSON_SUFFIX, ""); + } + + private void handleResult(StoredDataResult storedDataResult, long startTime, MetricName refreshType) { + cacheNotificationListener.save(storedDataResult.getStoredIdToRequest(), storedDataResult.getStoredIdToImp()); + metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); + } + + private void handleFailure(Throwable exception, long startTime, MetricName refreshType) { + logger.warn("Error occurred while request to s3 refresh service", exception); + + metrics.updateSettingsCacheRefreshTime(cacheType, refreshType, clock.millis() - startTime); + metrics.updateSettingsCacheRefreshErrorMetric(cacheType, refreshType); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 1006403c9c4..f7aaa9bb4ba 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -20,10 +20,12 @@ import org.prebid.server.settings.EnrichingApplicationSettings; import org.prebid.server.settings.FileApplicationSettings; import org.prebid.server.settings.HttpApplicationSettings; +import org.prebid.server.settings.S3ApplicationSettings; import org.prebid.server.settings.SettingsCache; import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.service.DatabasePeriodicRefreshService; import org.prebid.server.settings.service.HttpPeriodicRefreshService; +import org.prebid.server.settings.service.S3PeriodicRefreshService; import org.prebid.server.spring.config.database.DatabaseConfiguration; import org.prebid.server.vertx.database.DatabaseClient; import org.prebid.server.vertx.httpclient.HttpClient; @@ -37,12 +39,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Clock; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; @UtilityClass @@ -217,6 +227,115 @@ public DatabasePeriodicRefreshService ampDatabasePeriodicRefreshService( } } + @Configuration + @ConditionalOnProperty(prefix = "settings.s3", name = {"accounts-dir", "stored-imps-dir", "stored-requests-dir"}) + static class S3SettingsConfiguration { + + @Component + @ConfigurationProperties(prefix = "settings.s3") + @ConditionalOnProperty(prefix = "settings.s3", name = {"accessKeyId", "secretAccessKey"}) + @Validated + @Data + @NoArgsConstructor + protected static class S3ConfigurationProperties { + + @NotBlank + private String accessKeyId; + + @NotBlank + private String secretAccessKey; + + /** + * If not provided AWS_GLOBAL will be used as a region + */ + private String region; + + @NotBlank + private String endpoint; + + @NotBlank + private String bucket; + + @NotBlank + private Boolean forcePathStyle; + + @NotBlank + private String accountsDir; + + @NotBlank + private String storedImpsDir; + + @NotBlank + private String storedRequestsDir; + + @NotBlank + private String storedResponsesDir; + } + + @Bean + S3AsyncClient s3AsyncClient(S3ConfigurationProperties s3ConfigurationProperties) throws URISyntaxException { + final AwsBasicCredentials credentials = AwsBasicCredentials.create( + s3ConfigurationProperties.getAccessKeyId(), + s3ConfigurationProperties.getSecretAccessKey()); + final Region awsRegion = Optional.ofNullable(s3ConfigurationProperties.getRegion()) + .map(Region::of) + .orElse(Region.AWS_GLOBAL); + + return S3AsyncClient + .builder() + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .endpointOverride(new URI(s3ConfigurationProperties.getEndpoint())) + .forcePathStyle(s3ConfigurationProperties.getForcePathStyle()) + .region(awsRegion) + .build(); + } + + @Bean + S3ApplicationSettings s3ApplicationSettings(S3AsyncClient s3AsyncClient, + S3ConfigurationProperties s3ConfigurationProperties, + JacksonMapper mapper, + Vertx vertx) { + + return new S3ApplicationSettings( + s3AsyncClient, + s3ConfigurationProperties.getBucket(), + s3ConfigurationProperties.getAccountsDir(), + s3ConfigurationProperties.getStoredImpsDir(), + s3ConfigurationProperties.getStoredRequestsDir(), + s3ConfigurationProperties.getStoredResponsesDir(), + mapper, + vertx); + } + } + + @Configuration + @ConditionalOnProperty(prefix = "settings.in-memory-cache.s3-update", name = {"refresh-rate", "timeout"}) + static class S3PeriodicRefreshServiceConfiguration { + + @Bean + public S3PeriodicRefreshService s3PeriodicRefreshService( + S3AsyncClient s3AsyncClient, + S3SettingsConfiguration.S3ConfigurationProperties s3ConfigurationProperties, + @Value("${settings.in-memory-cache.s3-update.refresh-rate}") long refreshPeriod, + SettingsCache settingsCache, + Clock clock, + Metrics metrics, + Vertx vertx) { + + return new S3PeriodicRefreshService( + s3AsyncClient, + s3ConfigurationProperties.getBucket(), + s3ConfigurationProperties.getStoredRequestsDir(), + s3ConfigurationProperties.getStoredImpsDir(), + refreshPeriod, + settingsCache, + MetricName.stored_request, + clock, + metrics, + vertx); + } + } + /** * This configuration defines a collection of application settings fetchers and its ordering. */ @@ -227,14 +346,16 @@ static class CompositeSettingsConfiguration { CompositeApplicationSettings compositeApplicationSettings( @Autowired(required = false) FileApplicationSettings fileApplicationSettings, @Autowired(required = false) DatabaseApplicationSettings databaseApplicationSettings, - @Autowired(required = false) HttpApplicationSettings httpApplicationSettings) { + @Autowired(required = false) HttpApplicationSettings httpApplicationSettings, + @Autowired(required = false) S3ApplicationSettings s3ApplicationSettings) { - final List applicationSettingsList = - Stream.of(fileApplicationSettings, - databaseApplicationSettings, - httpApplicationSettings) - .filter(Objects::nonNull) - .toList(); + final List applicationSettingsList = Stream.of( + fileApplicationSettings, + databaseApplicationSettings, + s3ApplicationSettings, + httpApplicationSettings) + .filter(Objects::nonNull) + .toList(); return new CompositeApplicationSettings(applicationSettingsList); } @@ -338,7 +459,7 @@ SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperti @Validated @Data @NoArgsConstructor - private static class ApplicationSettingsCacheProperties { + protected static class ApplicationSettingsCacheProperties { @NotNull @Min(1) diff --git a/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy new file mode 100644 index 00000000000..4a25b6d6ca0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/service/S3Service.groovy @@ -0,0 +1,103 @@ +package org.prebid.server.functional.service + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.util.ObjectMapperWrapper +import org.testcontainers.containers.localstack.LocalStackContainer +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.CreateBucketRequest +import software.amazon.awssdk.services.s3.model.DeleteBucketRequest +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import software.amazon.awssdk.services.s3.model.PutObjectResponse + +final class S3Service implements ObjectMapperWrapper { + + private final S3Client s3PbsService + private final LocalStackContainer localStackContainer + + static final def DEFAULT_ACCOUNT_DIR = 'account' + static final def DEFAULT_IMPS_DIR = 'stored-impressions' + static final def DEFAULT_REQUEST_DIR = 'stored-requests' + static final def DEFAULT_RESPONSE_DIR = 'stored-responses' + + S3Service(LocalStackContainer localStackContainer) { + this.localStackContainer = localStackContainer + s3PbsService = S3Client.builder() + .endpointOverride(localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create( + localStackContainer.getAccessKey(), + localStackContainer.getSecretKey()))) + .region(Region.of(localStackContainer.getRegion())) + .build() + } + + String getAccessKeyId() { + localStackContainer.accessKey + } + + String getSecretKeyId() { + localStackContainer.secretKey + } + + String getEndpoint() { + "http://${localStackContainer.getNetworkAliases().get(0)}:${localStackContainer.getExposedPorts().get(0)}" + } + + String getRegion() { + localStackContainer.region + } + + void createBucket(String bucketName) { + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .build() + s3PbsService.createBucket(createBucketRequest) + } + + void deleteBucket(String bucketName) { + DeleteBucketRequest deleteBucketRequest = DeleteBucketRequest.builder() + .bucket(bucketName) + .build() + s3PbsService.deleteBucket(deleteBucketRequest) + } + + void purgeBucketFiles(String bucketName) { + s3PbsService.listObjectsV2(ListObjectsV2Request.builder().bucket(bucketName).build()).contents().each { files -> + s3PbsService.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(files.key()).build()) + } + } + + PutObjectResponse uploadAccount(String bucketName, AccountConfig account, String fileName = account.id) { + uploadFile(bucketName, encode(account), "${DEFAULT_ACCOUNT_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredRequest(String bucketName, StoredRequest storedRequest, String fileName = storedRequest.requestId) { + uploadFile(bucketName, encode(storedRequest.requestData), "${DEFAULT_REQUEST_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredResponse(String bucketName, StoredResponse storedRequest, String fileName = storedRequest.responseId) { + uploadFile(bucketName, encode(storedRequest.storedAuctionResponse), "${DEFAULT_RESPONSE_DIR}/${fileName}.json") + } + + PutObjectResponse uploadStoredImp(String bucketName, StoredImp storedImp, String fileName = storedImp.impId) { + uploadFile(bucketName, encode(storedImp.impData), "${DEFAULT_IMPS_DIR}/${fileName}.json") + } + + PutObjectResponse uploadFile(String bucketName, String fileBody, String path) { + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(path) + .build() + s3PbsService.putObject(putObjectRequest, RequestBody.fromString(fileBody)) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy index 53cbecf2289..ef2575ea3ed 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy @@ -4,8 +4,10 @@ import org.prebid.server.functional.testcontainers.container.NetworkServiceConta import org.prebid.server.functional.util.SystemProperties import org.testcontainers.containers.MySQLContainer import org.testcontainers.containers.Network +import org.testcontainers.containers.localstack.LocalStackContainer import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.lifecycle.Startables +import org.testcontainers.utility.DockerImageName import static org.prebid.server.functional.util.SystemProperties.MOCKSERVER_VERSION @@ -34,16 +36,20 @@ class Dependencies { static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION) .withNetwork(network) + static final LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) + .withNetwork(Dependencies.network) + .withServices(LocalStackContainer.Service.S3) + static void start() { if (IS_LAUNCH_CONTAINERS) { - Startables.deepStart([networkServiceContainer, mysqlContainer]) + Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]) .join() } } static void stop() { if (IS_LAUNCH_CONTAINERS) { - [networkServiceContainer, mysqlContainer].parallelStream() + [networkServiceContainer, mysqlContainer, localStackContainer].parallelStream() .forEach({ it.stop() }) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy new file mode 100644 index 00000000000..3a87be7b9e7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AccountS3Spec.groovy @@ -0,0 +1,118 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.AccountStatus +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.util.PBSUtils + +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED + +class AccountS3Spec extends StorageBaseSpec { + + protected PrebidServerService s3StorageAccountPbsService = PbsServiceFactory.getService(s3StorageConfig + + mySqlDisabledConfig + + ['settings.enforce-valid-account': 'true']) + + def "PBS should process request when active account is present in S3 storage"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Active account config" + def account = new AccountConfig(id: accountId, status: AccountStatus.ACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account) + + when: "PBS processes auction request" + def response = s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatbid" + assert response.seatbid.size() == 1 + } + + def "PBS should throw exception when inactive account is present in S3 storage"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Inactive account config" + def account = new AccountConfig(id: accountId, status: AccountStatus.INACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account) + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Account $accountId is inactive" + } + + def "PBS should throw exception when account id isn't match with bid request account id"() { + given: "Default BidRequest with account" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Account config with different accountId" + def account = new AccountConfig(id: PBSUtils.randomString, status: AccountStatus.ACTIVE) + + and: "Saved account in AWS S3 storage" + s3Service.uploadAccount(DEFAULT_BUCKET, account, accountId) + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } + + def "PBS should throw exception when account is invalid in S3 storage json file"() { + given: "Default BidRequest" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + and: "Saved invalid account in AWS S3 storage" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_ACCOUNT_DIR}/${accountId}.json") + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } + + def "PBS should throw exception when account is not present in S3 storage and valid account enforced"() { + given: "Default BidRequest" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + setAccountId(accountId) + } + + when: "PBS processes auction request" + s3StorageAccountPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: ${accountId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy new file mode 100644 index 00000000000..e6dda6b407c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AmpS3Spec.groovy @@ -0,0 +1,115 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class AmpS3Spec extends StorageBaseSpec { + + def "PBS should take parameters from the stored request on S3 service when it's not specified in the request"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest) + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain parameters from the stored request" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + assert bidderRequest.site?.page == ampStoredRequest.site.page + assert bidderRequest.site?.publisher?.id == ampStoredRequest.site.publisher.id + assert !bidderRequest.imp[0]?.tagId + assert bidderRequest.imp[0]?.banner?.format[0]?.height == ampStoredRequest.imp[0].banner.format[0].height + assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr + } + + @PendingFeature + def "PBS should throw exception when trying to take parameters from the stored request on S3 service with invalid id in file"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest).tap { + it.requestId = PBSUtils.randomNumber + } + s3Service.uploadStoredRequest(DEFAULT_BUCKET, storedRequest, ampRequest.tagId) + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored request found for id: ${ampRequest.tagId}" + } + + def "PBS should throw exception when trying to take parameters from request where id isn't match with stored request id"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + setAccountId(ampRequest.account) + } + + and: "Stored request in S3 service" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_REQUEST_DIR}/${ampRequest.tagId}.json") + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "Can't parse Json for stored request with id ${ampRequest.tagId}" + } + + def "PBS should throw an exception when trying to take parameters from stored request on S3 service that do not exist"() { + given: "AMP request" + def ampRequest = new AmpRequest(tagId: PBSUtils.randomString).tap { + account = PBSUtils.randomNumber as String + } + + when: "PBS processes amp request" + s3StoragePbsService.sendAmpRequest(ampRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored request found for id: ${ampRequest.tagId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy new file mode 100644 index 00000000000..51d39dd5af9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/AuctionS3Spec.groovy @@ -0,0 +1,117 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.SecurityLevel +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class AuctionS3Spec extends StorageBaseSpec { + + def "PBS auction should populate imp[0].secure depend which value in imp stored request from S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp into S3 service" + def secureStoredRequest = PBSUtils.getRandomEnum(SecurityLevel.class) + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression.tap { + secure = secureStoredRequest + } + } + s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain imp[0].secure same value as in request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].secure == secureStoredRequest + } + + @PendingFeature + def "PBS should throw exception when trying to populate imp[0].secure from imp stored request on S3 service with impId that doesn't matches"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp with different impId into S3 service" + def secureStoredRequest = PBSUtils.getRandomNumber(0, 1) + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impId = PBSUtils.randomString + impData = Imp.defaultImpression.tap { + it.secure = secureStoredRequest + } + } + s3Service.uploadStoredImp(DEFAULT_BUCKET, storedImp, storedRequestId) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored impression found for id: ${storedRequestId}" + } + + def "PBS should throw exception when trying to populate imp[0].secure from invalid imp stored request on S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + and: "Save storedImp into S3 service" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_IMPS_DIR}/${storedRequestId}.json" ) + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "Can't parse Json for stored request with id ${storedRequestId}" + } + + def "PBS should throw exception when trying to populate imp[0].secure from unexciting imp stored request on S3 service"() { + given: "Default bid request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].tap { + it.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.secure = null + } + } + + when: "Requesting PBS auction" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Stored request processing failed: " + + "No stored impression found for id: ${storedRequestId}" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy new file mode 100644 index 00000000000..583d6d97e06 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StorageBaseSpec.groovy @@ -0,0 +1,56 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.testcontainers.Dependencies +import org.prebid.server.functional.testcontainers.PbsServiceFactory +import org.prebid.server.functional.tests.BaseSpec +import org.prebid.server.functional.util.PBSUtils + +class StorageBaseSpec extends BaseSpec { + + protected static final String INVALID_FILE_BODY = 'INVALID' + protected static final String DEFAULT_BUCKET = PBSUtils.randomString.toLowerCase() + + protected static final S3Service s3Service = new S3Service(Dependencies.localStackContainer) + + def setupSpec() { + s3Service.createBucket(DEFAULT_BUCKET) + } + + def cleanupSpec() { + s3Service.purgeBucketFiles(DEFAULT_BUCKET) + s3Service.deleteBucket(DEFAULT_BUCKET) + } + + protected static Map s3StorageConfig = [ + 'settings.s3.accessKeyId' : s3Service.accessKeyId, + 'settings.s3.secretAccessKey' : s3Service.secretKeyId, + 'settings.s3.endpoint' : s3Service.endpoint, + 'settings.s3.bucket' : DEFAULT_BUCKET, + 'settings.s3.region' : s3Service.region, + 'settings.s3.force-path-style' : 'true', + 'settings.s3.accounts-dir' : S3Service.DEFAULT_ACCOUNT_DIR, + 'settings.s3.stored-imps-dir' : S3Service.DEFAULT_IMPS_DIR, + 'settings.s3.stored-requests-dir' : S3Service.DEFAULT_REQUEST_DIR, + 'settings.s3.stored-responses-dir': S3Service.DEFAULT_RESPONSE_DIR, + ] + + protected static Map mySqlDisabledConfig = + ['settings.database.type' : null, + 'settings.database.host' : null, + 'settings.database.port' : null, + 'settings.database.dbname' : null, + 'settings.database.user' : null, + 'settings.database.password' : null, + 'settings.database.pool-size' : null, + 'settings.database.provider-class' : null, + 'settings.database.account-query' : null, + 'settings.database.stored-requests-query' : null, + 'settings.database.amp-stored-requests-query': null, + 'settings.database.stored-responses-query' : null + ].asImmutable() as Map + + + protected PrebidServerService s3StoragePbsService = PbsServiceFactory.getService(s3StorageConfig + mySqlDisabledConfig) +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy new file mode 100644 index 00000000000..e07b5b71f2e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/storage/StoredResponseS3Spec.groovy @@ -0,0 +1,99 @@ +package org.prebid.server.functional.tests.storage + +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.S3Service +import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST + +class StoredResponseS3Spec extends StorageBaseSpec { + + def "PBS should return info from S3 stored auction response when it defined in request"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Stored auction response in S3 storage" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: storedAuctionResponse) + s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse) + + when: "PBS processes auction request" + def response = s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored auction response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedAuctionResponse.seat + assert response.seatbid[0]?.bid?.size() == storedAuctionResponse.bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedAuctionResponse.bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedAuctionResponse.bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedAuctionResponse.bid[0].id + + and: "PBS not send request to bidder" + assert !bidder.getRequestCount(bidRequest.id) + } + + @PendingFeature + def "PBS should throw request format exception when stored auction response id isn't match with requested response id"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Stored auction response in S3 storage with different id" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: PBSUtils.randomNumber, + storedAuctionResponse: storedAuctionResponse) + s3Service.uploadStoredResponse(DEFAULT_BUCKET, storedResponse, storedResponseId as String) + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " + + "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}." + } + + def "PBS should throw request format exception when invalid stored auction response defined in S3 storage"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + and: "Invalid stored auction response in S3 storage" + s3Service.uploadFile(DEFAULT_BUCKET, INVALID_FILE_BODY, "${S3Service.DEFAULT_RESPONSE_DIR}/${storedResponseId}.json") + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Can't parse Json for stored response with id ${storedResponseId}" + } + + def "PBS should throw request format exception when stored auction response defined in request but not defined in S3 storage"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedResponseId = PBSUtils.randomNumber + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + + when: "PBS processes auction request" + s3StoragePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw request format error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: Failed to fetch stored auction response for " + + "impId = ${bidRequest.imp[0].id} and storedAuctionResponse id = ${storedResponseId}." + } +} diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 13944033ed1..674a3fcf245 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -1223,7 +1223,6 @@ public void shouldReturnEmptyAssetIfNoRelatedNativeAssetFound() throws JsonProce final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - assertThat(bidResponse.getSeatbid()).hasSize(1) .flatExtracting(SeatBid::getBid) .extracting(Bid::getAdm) @@ -1283,7 +1282,6 @@ public void shouldReturnEmptyAssetIfIdIsNotPresentRelatedNativeAssetFound() thro final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - assertThat(bidResponse.getSeatbid()).hasSize(1) .flatExtracting(SeatBid::getBid) .extracting(Bid::getAdm) diff --git a/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java b/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java index 52495c635da..ab68ca9a517 100644 --- a/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/algorix/AlgorixBidderTest.java @@ -67,7 +67,6 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { @Test public void makeHttpRequestsShouldReturnErrorOfEveryNotValidImp() { // given - final BidRequest bidRequest = BidRequest.builder() .imp(asList(Imp.builder() .id("123") diff --git a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java index 9ad08ecb30a..6de8d3f4d65 100644 --- a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java @@ -221,7 +221,6 @@ public void makeHttpRequestsShouldModifyImpWithAddingSkadnWhenSkadnIsPresent() { final Result>> result = target.makeHttpRequests(bidRequest); // then - final ObjectNode expectedImpExt = mapper.createObjectNode().set("skadn", skadn); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) diff --git a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java index ed4731a5320..e6429a971d1 100644 --- a/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidderTest.java @@ -184,7 +184,6 @@ public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBi @Test public void makeHttpRequestsShouldTakePriceFloorsWhenBidfloorParamIsAlsoPresent() { // given - final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .bidfloor(BigDecimal.TEN).bidfloorcur("USD") @@ -209,7 +208,6 @@ public void makeHttpRequestsShouldTakePriceFloorsWhenBidfloorParamIsAlsoPresent( @Test public void makeHttpRequestsShouldTakeBidfloorExtImpParamIfNoBidfloorInRequest() { // given - final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() .ext(mapper.valueToTree(ExtPrebid.of(null, diff --git a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java index 24531719f48..18b1ba36e8a 100644 --- a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java @@ -921,7 +921,6 @@ public void makeBidsShouldReturnCorrectBidIfAdMarkTypeIsNative() throws JsonProc final Result> result = target.makeBids(httpCall, null); // then - final String expectedAdm = "{\"assets\":[{\"id\":1,\"img\":{\"type\":3," + "\"url\":\"https://smaato.com/image.png\",\"w\":480,\"h\":320}}]," + "\"link\":{\"url\":\"https://www.smaato.com\"}}"; diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 8c35236c361..8a55773a0c0 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -646,7 +646,6 @@ public void cacheBidsOpenrtbShouldNotUpdateVastXmlPutObjectWithKeyWhenDoesNotHav @Test public void cacheBidsOpenrtbShouldRemoveCatDurPrefixFromVideoUuidFromResponse() throws IOException { // given - givenHttpClientReturnsResponse(200, mapper.writeValueAsString( BidCacheResponse.of(asList(CacheObject.of("uuid"), CacheObject.of("catDir_randomId"))))); final BidInfo bidInfo1 = givenBidInfo(builder -> builder.id("bid1").impid("impId1").adm("adm"), diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 70835b4286a..0990d97b5aa 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -493,7 +493,6 @@ public void shouldNotUpdateImpsIfBidFloorNotResolved() { @Test public void shouldUpdateImpsIfBidFloorResolved() { // given - final PriceFloorRules requestFloors = givenFloors(floors -> floors .data(givenFloorData(floorData -> floorData .modelGroups(singletonList(givenModelGroup(identity())))))); @@ -511,7 +510,6 @@ public void shouldUpdateImpsIfBidFloorResolved() { .willReturn(PriceFloorResult.of("rule", BigDecimal.ONE, BigDecimal.TEN, "USD")); // when - final BidRequest result = target.enrichWithPriceFloors( givenBidRequest(request -> request.imp(imps), requestFloors), givenAccount(identity()), diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index a5af550a562..9f40ed1bc81 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -726,7 +726,6 @@ public void shouldIncrementErrAmpRequestMetrics() { @Test public void shouldUpdateRequestTimeMetric() { // given - // set up clock mock to check that request_time metric has been updated with expected value given(clock.millis()).willReturn(5000L).willReturn(5500L); diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index 87396cca90d..c2a84bcc922 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -587,7 +587,6 @@ public void shouldIncrementErrOpenrtb2WebRequestMetrics() { @Test public void shouldUpdateRequestTimeMetric() { // given - // set up clock mock to check that request_time metric has been updated with expected value given(clock.millis()).willReturn(5000L).willReturn(5500L); diff --git a/src/test/java/org/prebid/server/it/AdnuntiusTest.java b/src/test/java/org/prebid/server/it/AdnuntiusTest.java index 23bb6000371..9a03d73ccf5 100644 --- a/src/test/java/org/prebid/server/it/AdnuntiusTest.java +++ b/src/test/java/org/prebid/server/it/AdnuntiusTest.java @@ -18,7 +18,6 @@ public class AdnuntiusTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdnuntius() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adnuntius-exchange")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adnuntius/test-adnuntius-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adnuntius/test-adnuntius-bid-response.json")))); diff --git a/src/test/java/org/prebid/server/it/MinuteMediaTest.java b/src/test/java/org/prebid/server/it/MinuteMediaTest.java index 0aace384eb5..2f8c591dc08 100644 --- a/src/test/java/org/prebid/server/it/MinuteMediaTest.java +++ b/src/test/java/org/prebid/server/it/MinuteMediaTest.java @@ -19,7 +19,6 @@ public class MinuteMediaTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromMinuteMedia() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/minutemedia-exchange")) .withQueryParam("publisherId", equalTo("123")) .withRequestBody(equalToJson(jsonFrom("openrtb2/minutemedia/test-minutemedia-bid-request.json"))) diff --git a/src/test/java/org/prebid/server/it/PrecisoTest.java b/src/test/java/org/prebid/server/it/PrecisoTest.java index 5d97978e7b7..da461016e64 100644 --- a/src/test/java/org/prebid/server/it/PrecisoTest.java +++ b/src/test/java/org/prebid/server/it/PrecisoTest.java @@ -23,8 +23,8 @@ public void openrtb2AuctionShouldRespondWithBidsFromPreciso() throws IOException "openrtb2/preciso/test-preciso-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom( "openrtb2/preciso/test-preciso-bid-response.json")))); - // when + // when final Response response = responseFor("openrtb2/preciso/test-auction-preciso-request.json", Endpoint.openrtb2_auction); diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java index bac345c8906..d09df3327a8 100644 --- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java @@ -336,7 +336,6 @@ public void getCategoriesShouldNotCacheNotPreBidException() { .willReturn(Future.failedFuture(new TimeoutException("timeout"))); // when - target.getCategories("adServer", "publisher", timeout); target.getCategories("adServer", "publisher", timeout); final Future> lastFuture = diff --git a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java new file mode 100644 index 00000000000..2f7c293f9f8 --- /dev/null +++ b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java @@ -0,0 +1,403 @@ +package org.prebid.server.settings; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.AfterEach; +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.exception.PreBidException; +import org.prebid.server.execution.Timeout; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredResponseDataResult; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(VertxExtension.class) +public class S3ApplicationSettingsTest extends VertxTest { + + private static final String BUCKET = "bucket"; + private static final String ACCOUNTS_DIR = "accounts"; + private static final String STORED_IMPS_DIR = "stored-imps"; + private static final String STORED_REQUESTS_DIR = "stored-requests"; + private static final String STORED_RESPONSES_DIR = "stored-responses"; + + @Mock + private S3AsyncClient s3AsyncClient; + + private Vertx vertx; + + private S3ApplicationSettings target; + + @Mock + private Timeout timeout; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new S3ApplicationSettings( + s3AsyncClient, + BUCKET, + ACCOUNTS_DIR, + STORED_IMPS_DIR, + STORED_REQUESTS_DIR, + STORED_RESPONSES_DIR, + jacksonMapper, + vertx); + + given(timeout.remaining()).willReturn(500L); + } + + @AfterEach + public void tearDown(VertxTestContext context) { + vertx.close(context.succeedingThenComplete()); + } + + @Test + public void getAccountByIdShouldReturnFetchedAccount(VertxTestContext context) throws JsonProcessingException { + // given + final Account account = Account.builder().id("accountId").build(); + + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(ACCOUNTS_DIR, "accountId")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + mapper.writeValueAsString(account).getBytes()))); + + // when + final Future result = target.getAccountById("accountId", timeout); + + // then + result.onComplete(context.succeeding(returnedAccount -> { + assertThat(returnedAccount.getId()).isEqualTo("accountId"); + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnTimeout(VertxTestContext context) { + // given + given(timeout.remaining()).willReturn(-1L); + + // when + final Future result = target.getAccountById("account", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(TimeoutException.class) + .hasMessage("Timeout has been exceeded"); + + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnAccountNotFound(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getAccountById("notFoundId", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id notFoundId not found"); + + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnInvalidJson(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "invalidJson".getBytes()))); + + // when + final Future result = target.getAccountById("invalidJsonId", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(PreBidException.class) + .hasMessage("Invalid json for account with id invalidJsonId"); + + context.completeNow(); + })); + } + + @Test + public void getAccountByIdShouldReturnAccountIdMismatch(VertxTestContext context) throws JsonProcessingException { + // given + final Account account = Account.builder().id("accountId").build(); + + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(ACCOUNTS_DIR, "anotherAccountId")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + mapper.writeValueAsString(account).getBytes()))); + + // when + final Future result = target.getAccountById("anotherAccountId", timeout); + + // then + result.onComplete(context.failing(cause -> { + assertThat(cause) + .isInstanceOf(PreBidException.class) + .hasMessage("Account with id anotherAccountId does not match id accountId in file"); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredRequest(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_REQUESTS_DIR, "request")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedRequest".getBytes()))); + + // when + final Future result = target.getStoredData( + "accountId", Set.of("request"), emptySet(), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEqualTo(Map.of("request", "storedRequest")); + assertThat(storedDataResult.getStoredIdToImp()).isEmpty(); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredImpression(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_IMPS_DIR, "imp")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedImp".getBytes()))); + + // when + final Future result = target.getStoredData( + "accountId", emptySet(), Set.of("imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getStoredIdToImp()).isEqualTo(Map.of("imp", "storedImp")); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredImpressionWithAdUnitPath(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_IMPS_DIR, "imp")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedImp".getBytes()))); + + // when + final Future result = target.getStoredData("accountId", emptySet(), Set.of("/imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getStoredIdToImp()).isEqualTo(Map.of("/imp", "storedImp")); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnFetchedStoredRequestAndStoredImpression(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_REQUESTS_DIR, "request")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedRequest".getBytes()))); + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_IMPS_DIR, "imp")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedImp".getBytes()))); + + // when + final Future result = target.getStoredData( + "accountId", Set.of("request"), Set.of("imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToRequest()).isEqualTo(Map.of("request", "storedRequest")); + assertThat(storedDataResult.getStoredIdToImp()).isEqualTo(Map.of("imp", "storedImp")); + assertThat(storedDataResult.getErrors()).isEmpty(); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnErrorsForNotFoundRequests(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getStoredData( + "accountId", Set.of("request"), emptySet(), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToImp()).isEmpty(); + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getErrors()) + .isEqualTo(singletonList("No stored request found for id: request")); + + context.completeNow(); + })); + } + + @Test + public void getStoredDataShouldReturnErrorsForNotFoundImpressions(VertxTestContext context) { + // given + given(s3AsyncClient.getObject(any(GetObjectRequest.class), any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getStoredData( + "accountId", emptySet(), Set.of("imp"), timeout); + + // then + result.onComplete(context.succeeding(storedDataResult -> { + assertThat(storedDataResult.getStoredIdToImp()).isEmpty(); + assertThat(storedDataResult.getStoredIdToRequest()).isEmpty(); + assertThat(storedDataResult.getErrors()).isEqualTo(singletonList("No stored impression found for id: imp")); + + context.completeNow(); + })); + } + + @Test + public void getStoredResponsesShouldReturnExpectedResult(VertxTestContext context) { + // given + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_RESPONSES_DIR, "response1")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + "storedResponse1".getBytes()))); + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key("%s/%s.json".formatted(STORED_RESPONSES_DIR, "response2")) + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(CompletableFuture.failedFuture( + NoSuchKeyException.create( + "The specified key does not exist.", + new IllegalStateException("error")))); + + // when + final Future result = target.getStoredResponses( + Set.of("response1", "response2"), timeout); + + // then + result.onComplete(context.succeeding(storedResponseDataResult -> { + assertThat(storedResponseDataResult.getIdToStoredResponses()) + .isEqualTo(Map.of("response1", "storedResponse1")); + assertThat(storedResponseDataResult.getErrors()) + .isEqualTo(singletonList("No stored response found for id: response2")); + + context.completeNow(); + })); + } +} diff --git a/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java new file mode 100644 index 00000000000..e9b37a75d94 --- /dev/null +++ b/src/test/java/org/prebid/server/settings/service/S3PeriodicRefreshServiceTest.java @@ -0,0 +1,174 @@ +package org.prebid.server.settings.service; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.AfterEach; +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.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.CacheNotificationListener; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsResponse; +import software.amazon.awssdk.services.s3.model.S3Object; + +import java.time.Clock; +import java.util.concurrent.CompletableFuture; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@ExtendWith(VertxExtension.class) +public class S3PeriodicRefreshServiceTest extends VertxTest { + + private static final String BUCKET = "bucket"; + private static final String STORED_REQ_DIR = "stored-req"; + private static final String STORED_IMP_DIR = "stored-imp"; + + @Mock(strictness = LENIENT) + private S3AsyncClient s3AsyncClient; + + @Mock + private CacheNotificationListener cacheNotificationListener; + + @Mock + private Clock clock; + + @Mock + private Metrics metrics; + + private Vertx vertx; + + @BeforeEach + public void setUp() { + vertx = spy(Vertx.vertx()); + + given(s3AsyncClient.listObjects(eq(ListObjectsRequest.builder() + .bucket(BUCKET) + .prefix(STORED_REQ_DIR) + .build()))) + .willReturn(listObjectResponse(STORED_REQ_DIR + "/id1.json")); + given(s3AsyncClient.listObjects(eq(ListObjectsRequest.builder() + .bucket(BUCKET) + .prefix(STORED_IMP_DIR) + .build()))) + .willReturn(listObjectResponse(STORED_IMP_DIR + "/id2.json")); + + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key(STORED_REQ_DIR + "/id1.json") + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(getObjectResponse("value1")); + given(s3AsyncClient.getObject( + eq(GetObjectRequest.builder() + .bucket(BUCKET) + .key(STORED_IMP_DIR + "/id2.json") + .build()), + any(AsyncResponseTransformer.class))) + .willReturn(getObjectResponse("value2")); + + given(clock.millis()).willReturn(100L, 500L); + } + + @AfterEach + public void tearDown(VertxTestContext context) { + vertx.close(context.succeedingThenComplete()); + } + + @Test + public void initializeShouldCallSaveWithExpectedParameters(VertxTestContext context) { + // when and then + createAndInitService(100).onComplete(context.succeeding(ignored -> { + verify(cacheNotificationListener, atLeast(1)) + .save(singletonMap("id1", "value1"), singletonMap("id2", "value2")); + verify(metrics, atLeast(1)).updateSettingsCacheRefreshTime( + eq(MetricName.stored_request), eq(MetricName.initialize), eq(400L)); + + context.completeNow(); + })); + } + + @Test + public void initializeShouldNotCreatePeriodicTaskIfRefreshPeriodIsNegative(VertxTestContext context) { + // when and then + createAndInitService(-1).onComplete(context.succeeding(unused -> { + verify(vertx, never()).setPeriodic(anyLong(), any()); + + context.completeNow(); + })); + } + + @Test + public void initializeShouldUpdateMetricsOnError(VertxTestContext context) { + // given + given(s3AsyncClient.listObjects(any(ListObjectsRequest.class))) + .willReturn(CompletableFuture.failedFuture(new IllegalStateException("Failed"))); + + // when + createAndInitService(100).onComplete(context.failing(ignored -> { + verify(metrics, atLeast(1)).updateSettingsCacheRefreshTime( + eq(MetricName.stored_request), eq(MetricName.initialize), eq(400L)); + verify(metrics, atLeast(1)).updateSettingsCacheRefreshErrorMetric( + eq(MetricName.stored_request), eq(MetricName.initialize)); + + context.completeNow(); + })); + } + + private CompletableFuture listObjectResponse(String key) { + return CompletableFuture.completedFuture( + ListObjectsResponse + .builder() + .contents(singletonList(S3Object.builder().key(key).build())) + .build()); + } + + private CompletableFuture> getObjectResponse(String value) { + return CompletableFuture.completedFuture( + ResponseBytes.fromByteArray( + GetObjectResponse.builder().build(), + value.getBytes())); + } + + private Future createAndInitService(long refreshPeriod) { + final S3PeriodicRefreshService s3PeriodicRefreshService = new S3PeriodicRefreshService( + s3AsyncClient, + BUCKET, + STORED_REQ_DIR, + STORED_IMP_DIR, + refreshPeriod, + cacheNotificationListener, + MetricName.stored_request, + clock, + metrics, + vertx); + + final Promise init = Promise.promise(); + s3PeriodicRefreshService.initialize(init); + return init.future(); + } +} From 645468396c216d9f1858b57497020579a7349fb6 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Mon, 9 Sep 2024 07:39:20 -0400 Subject: [PATCH 051/170] Docs: Dockerfile documentation clarifications (#3427) --- README.md | 26 ++++++++++++++++++++++---- src/main/docker/run.sh | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f4c627c6c4f..9fbfe912715 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,30 @@ There are a couple of 'hello world' test requests described in sample/requests/R ## Running Docker image -Starting from PBS Java v2.9, you can download prebuilt Docker images from [GitHub Packages](https://github.com/orgs/prebid/packages?repo_name=prebid-server-java) page, -and use them instead of plain .jar files. This prebuilt images are delivered with or without extra modules. +Starting from PBS Java v3.11.0, you can download prebuilt Docker images from [GitHub Packages](https://github.com/orgs/prebid/packages?repo_name=prebid-server-java) page, +and use them instead of plain .jar files. These prebuilt images are delivered in 2 flavors: +- https://github.com/prebid/prebid-server-java/pkgs/container/prebid-server-java is a bare PBS and doesn't contain modules. +- https://github.com/prebid/prebid-server-java/pkgs/container/prebid-server-java-bundle is a "bundle" that contains PBS and all the modules. -In order to run such image correctly, you should attach PBS config file. Easiest way is to mount config file into container, +To run PBS from image correctly, you should provide the PBS config file. The easiest way is to mount the config file into the container, using [--mount or --volume (-v) Docker CLI arguments](https://docs.docker.com/engine/reference/commandline/run/). -Keep in mind, that config file should be mounted into specific location: ```/app/prebid-server/``` or ```/app/prebid-server/conf/```. +Keep in mind that the config file should be mounted into a specific location: ```/app/prebid-server/conf/``` or ```/app/prebid-server/```. + +PBS follows the regular Spring Boot config load hierarchy and type. +For simple configuration, a single `application.yaml` mounted to `/app/prebid-server/conf/` will be enough. +Please consult [Spring Externalized Configuration](https://docs.spring.io/spring-boot/reference/features/external-config.html) for all possible ways to configure PBS. + +You can also supply command-line parameters through `JAVA_OPTS` environment variable which will be appended to the `java` command before the `-jar ...` parameter. +Please pay attention to line breaks and escape them if needed. + +Example execution using sample configuration: +```shell +docker run --rm -v ./sample:/app/prebid-server/sample:ro -p 8060:8060 -p 8080:8080 ghcr.io/prebid/prebid-server-java:latest --spring.config.additional-location=sample/configs/prebid-config.yaml +``` +or +```shell +docker run --rm -v ./sample:/app/prebid-server/sample:ro -p 8060:8060 -p 8080:8080 -e JAVA_OPTS=-Dspring.config.additional-location=sample/configs/prebid-config.yaml ghcr.io/prebid/prebid-server-java:latest +``` # Documentation diff --git a/src/main/docker/run.sh b/src/main/docker/run.sh index 54f73437643..884aa8b3fd1 100755 --- a/src/main/docker/run.sh +++ b/src/main/docker/run.sh @@ -5,4 +5,4 @@ exec java \ -Dspring.config.additional-location=/app/prebid-server/,/app/prebid-server/conf/ \ ${JAVA_OPTS} \ -jar \ - /app/prebid-server/prebid-server.jar + /app/prebid-server/prebid-server.jar "$@" From 411bd33763e3545f72b221363099a57af650d014 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Mon, 9 Sep 2024 14:15:51 +0200 Subject: [PATCH 052/170] Copper6Ssp: New adapter (#3428) --- .../bidder/copper6ssp/Copper6SspBidder.java | 138 +++++++ .../proto/Copper6SspImpExtBidder.java | 18 + .../request/copper6ssp/ImpExtCopper6Ssp.java | 15 + .../bidder/Copper6SspConfiguration.java | 41 +++ .../resources/bidder-config/copper6ssp.yaml | 25 ++ .../static/bidder-params/copper6ssp.json | 30 ++ .../copper6ssp/Copper6SspBidderTest.java | 337 ++++++++++++++++++ .../org/prebid/server/it/Copper6SspTest.java | 33 ++ .../test-auction-copper6ssp-request.json | 26 ++ .../test-auction-copper6ssp-response.json | 37 ++ .../test-copper6ssp-bid-request.json | 59 +++ .../test-copper6ssp-bid-response.json | 20 ++ .../server/it/test-application.properties | 2 + 13 files changed, 781 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java create mode 100644 src/main/resources/bidder-config/copper6ssp.yaml create mode 100644 src/main/resources/static/bidder-params/copper6ssp.json create mode 100644 src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/Copper6SspTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java b/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java new file mode 100644 index 00000000000..f8d739193a9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.copper6ssp; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.copper6ssp.proto.Copper6SspImpExtBidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.copper6ssp.ImpExtCopper6Ssp; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class Copper6SspBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public Copper6SspBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> outgoingRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + final ImpExtCopper6Ssp extImp; + try { + extImp = parseImpExt(imp); + outgoingRequests.add(makeRequest(modifyImp(imp, extImp), request)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return CollectionUtils.isEmpty(outgoingRequests) + ? Result.withErrors(errors) + : Result.of(outgoingRequests, errors); + } + + private ImpExtCopper6Ssp parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ImpExtCopper6Ssp extImp) { + final Copper6SspImpExtBidder impExtBidder = getImpExtWithType(extImp); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + + modifiedImpExtBidder.set("bidder", mapper.mapper().valueToTree(impExtBidder)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private Copper6SspImpExtBidder getImpExtWithType(ImpExtCopper6Ssp impExtCopper6Ssp) { + final boolean hasPlacementId = StringUtils.isNotBlank(impExtCopper6Ssp.getPlacementId()); + final boolean hasEndpointId = StringUtils.isNotBlank(impExtCopper6Ssp.getEndpointId()); + + return Copper6SspImpExtBidder.builder() + .type(hasPlacementId ? "publisher" : hasEndpointId ? "network" : null) + .placementId(hasPlacementId ? impExtCopper6Ssp.getPlacementId() : null) + .endpointId(hasEndpointId ? impExtCopper6Ssp.getEndpointId() : null) + .build(); + } + + private HttpRequest makeRequest(Imp imp, BidRequest request) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid).filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + throw new PreBidException("Missing MType for bid: " + bid.getId()); + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> throw new PreBidException("Unable to fetch mediaType in multi-format: %s" + .formatted(bid.getImpid())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java b/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java new file mode 100644 index 00000000000..5feb52a144b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/copper6ssp/proto/Copper6SspImpExtBidder.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.copper6ssp.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class Copper6SspImpExtBidder { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java new file mode 100644 index 00000000000..1c2c057878a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request.copper6ssp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ImpExtCopper6Ssp { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} + diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java new file mode 100644 index 00000000000..61340987965 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/Copper6SspConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.copper6ssp.Copper6SspBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/copper6ssp.yaml", factory = YamlPropertySourceFactory.class) +public class Copper6SspConfiguration { + + private static final String BIDDER_NAME = "copper6ssp"; + + @Bean("copper6sspConfigurationProperties") + @ConfigurationProperties("adapters.copper6ssp") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps copper6sspBidderDeps(BidderConfigurationProperties copper6sspConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(copper6sspConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new Copper6SspBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/copper6ssp.yaml b/src/main/resources/bidder-config/copper6ssp.yaml new file mode 100644 index 00000000000..bc7ceceb4b4 --- /dev/null +++ b/src/main/resources/bidder-config/copper6ssp.yaml @@ -0,0 +1,25 @@ +adapters: + copper6ssp: + endpoint: https://endpoint.copper6.com/ + meta-info: + maintainer-email: info@copper6.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: copper6ssp + redirect: + support-cors: false + url: https://csync.copper6.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + uid-macro: '[UID]' + iframe: + support-cors: false + url: https://csync.copper6.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' diff --git a/src/main/resources/static/bidder-params/copper6ssp.json b/src/main/resources/static/bidder-params/copper6ssp.json new file mode 100644 index 00000000000..e17c3f38ce7 --- /dev/null +++ b/src/main/resources/static/bidder-params/copper6ssp.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Copper6SSPs Adapter Params", + "description": "A schema which validates params accepted by the Copper6SSP adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java b/src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java new file mode 100644 index 00000000000..0a08b03a6aa --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/copper6ssp/Copper6SspBidderTest.java @@ -0,0 +1,337 @@ +package org.prebid.server.bidder.copper6ssp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.copper6ssp.proto.Copper6SspImpExtBidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.copper6ssp.ImpExtCopper6Ssp; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class Copper6SspBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/"; + + private final Copper6SspBidder target = new Copper6SspBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new Copper6SspBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com/"); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Collections.singleton("givenImp1"), Collections.singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestWhenOneImpIsValidAndAnotherIsNot() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("validImpId")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("validImpId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenNoValidImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp + .id("invalidImpId") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + //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("Cannot deserialize value of type"); + }); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(1); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypePublisher() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ImpExtCopper6Ssp.of("somePlacementId", ""))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("publisher").placementId("somePlacementId"))); + } + + @Test + public void makeHttpRequestsShouldReturnExtTypeNetwork() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, + ImpExtCopper6Ssp.of("", "someEndpointId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(givenImpExt(ext -> ext.type("network").endpointId("someEndpointId"))); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnxNativeBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(4).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("impId").mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("impId").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("impId").build(), video, "USD")); + } + + @Test + public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bidBuilder -> bidBuilder.impid("impId"))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Missing MType for bid: null"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(Copper6SspBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ImpExtCopper6Ssp.of("placementId", "endpointId"))))) + .build(); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private ObjectNode givenImpExt(UnaryOperator impExt) { + final ObjectNode modifiedImpExtBidder = mapper.createObjectNode(); + + return modifiedImpExtBidder.set("bidder", mapper.convertValue( + impExt.apply(Copper6SspImpExtBidder.builder()).build(), + JsonNode.class)); + } +} diff --git a/src/test/java/org/prebid/server/it/Copper6SspTest.java b/src/test/java/org/prebid/server/it/Copper6SspTest.java new file mode 100644 index 00000000000..dad5da9c05b --- /dev/null +++ b/src/test/java/org/prebid/server/it/Copper6SspTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class Copper6SspTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromCopper6Ssp() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/copper6ssp-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/copper6ssp/test-copper6ssp-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/copper6ssp/test-copper6ssp-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/copper6ssp/test-auction-copper6ssp-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/copper6ssp/test-auction-copper6ssp-response.json", response, + singletonList("copper6ssp")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json new file mode 100644 index 00000000000..97375afbc45 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "copper6ssp": { + "endpointId": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json new file mode 100644 index 00000000000..fb24eb9368c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + }, + "mtype": 2 + } + ], + "seat": "copper6ssp", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "copper6ssp": "{{ copper6ssp.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json new file mode 100644 index 00000000000..5da47810a6b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "bidder": { + "type": "network", + "endpointId": "test" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-copper6ssp-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} 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 cb54ca7386e..9369f0455b8 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -155,6 +155,8 @@ adapters.coinzilla.enabled=true adapters.coinzilla.endpoint=http://localhost:8090/coinzilla-exchange adapters.consumable.enabled=true adapters.consumable.endpoint=http://localhost:8090/consumable-exchange +adapters.copper6ssp.enabled=true +adapters.copper6ssp.endpoint=http://localhost:8090/copper6ssp-exchange adapters.criteo.enabled=true adapters.criteo.endpoint=http://localhost:8090/criteo-exchange adapters.criteo.generate-slot-id=false From d5c1ae61ade884749e203ebb7cffc20f0e75d5d1 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:21:04 +0200 Subject: [PATCH 053/170] Escalax: Add bidder (#3429) --- .../server/bidder/escalax/EscalaxBidder.java | 140 +++++++++ .../ext/request/escalax/ExtImpEscalax.java | 14 + .../config/bidder/EscalaxConfiguration.java | 41 +++ src/main/resources/bidder-config/aax.yaml | 2 +- src/main/resources/bidder-config/aidem.yaml | 2 +- src/main/resources/bidder-config/escalax.yaml | 17 ++ .../resources/bidder-config/freewheelssp.yaml | 2 +- src/main/resources/bidder-config/kargo.yaml | 2 +- src/main/resources/bidder-config/sovrn.yaml | 2 +- .../resources/bidder-config/sovrnXsp.yaml | 2 +- .../resources/bidder-config/trafficgate.yaml | 2 +- .../static/bidder-params/escalax.json | 22 ++ .../bidder/escalax/EscalaxBidderTest.java | 269 ++++++++++++++++++ .../org/prebid/server/it/EscalaxTest.java | 36 +++ .../escalax/test-auction-escalax-request.json | 27 ++ .../test-auction-escalax-response.json | 37 +++ .../escalax/test-escalax-bid-request.json | 53 ++++ .../escalax/test-escalax-bid-response.json | 20 ++ .../server/it/test-application.properties | 2 + 19 files changed, 685 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java create mode 100644 src/main/resources/bidder-config/escalax.yaml create mode 100644 src/main/resources/static/bidder-params/escalax.json create mode 100644 src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/EscalaxTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java b/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java new file mode 100644 index 00000000000..6d520a2ecd0 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/escalax/EscalaxBidder.java @@ -0,0 +1,140 @@ +package org.prebid.server.bidder.escalax; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.escalax.ExtImpEscalax; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ObjectUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +public class EscalaxBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String X_OPENRTB_VERSION = "2.5"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public EscalaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpEscalax extImp; + try { + extImp = parseImpExt(firstImp); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + return Result.withValue(makeHttpRequest(createRequest(request), extImp)); + } + + private static BidRequest createRequest(BidRequest request) { + return request.toBuilder().imp(prepareFirstImp(request.getImp())).build(); + } + + private static List prepareFirstImp(List imps) { + final Imp firstImp = imps.getFirst(); + final List updatedImps = new ArrayList<>(imps); + updatedImps.set(0, firstImp.toBuilder().ext(null).build()); + + return updatedImps; + } + + private HttpRequest makeHttpRequest(BidRequest bidRequest, ExtImpEscalax extImp) { + return BidderUtil.defaultRequest(bidRequest, makeHeaders(bidRequest.getDevice()), makeUrl(extImp), mapper); + } + + private String makeUrl(ExtImpEscalax extImp) { + return endpointUrl + .replace("{{AccountID}}", extImp.getAccountId()) + .replace("{{SourceId}}", extImp.getSourceId()); + } + + private MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + headers.set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, + ObjectUtil.getIfNotNull(device, Device::getUa)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIpv6)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIp)); + + return headers; + } + + private ExtImpEscalax parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing escalaxExt - " + e.getMessage()); + } + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + throw new PreBidException("Empty SeatBid array"); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("unsupported MType " + mtype); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java new file mode 100644 index 00000000000..03b14ab82eb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/escalax/ExtImpEscalax.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.escalax; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpEscalax { + + @JsonProperty("sourceId") + String sourceId; + + @JsonProperty("accountId") + String accountId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java new file mode 100644 index 00000000000..29bd855b91b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/EscalaxConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.escalax.EscalaxBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/escalax.yaml", factory = YamlPropertySourceFactory.class) +public class EscalaxConfiguration { + + private static final String BIDDER_NAME = "escalax"; + + @Bean("escalaxConfigurationProperties") + @ConfigurationProperties("adapters.escalax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps escalaxBidderDeps(BidderConfigurationProperties escalaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(escalaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new EscalaxBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/aax.yaml b/src/main/resources/bidder-config/aax.yaml index b83b0a9bcf6..b695864b8c2 100644 --- a/src/main/resources/bidder-config/aax.yaml +++ b/src/main/resources/bidder-config/aax.yaml @@ -1,7 +1,7 @@ adapters: aax: endpoint: https://prebid.aaxads.com/rtb/pb/aax-prebid?src={{PREBID_SERVER_ENDPOINT}} - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: product@aax.media app-media-types: diff --git a/src/main/resources/bidder-config/aidem.yaml b/src/main/resources/bidder-config/aidem.yaml index cd8b37239ab..9cd7c5432af 100644 --- a/src/main/resources/bidder-config/aidem.yaml +++ b/src/main/resources/bidder-config/aidem.yaml @@ -1,7 +1,7 @@ adapters: aidem: endpoint: https://zero.aidemsrv.com/ortb/v2.6/bid/request?billing_id={{PublisherId}} - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: prebid@aidem.com app-media-types: diff --git a/src/main/resources/bidder-config/escalax.yaml b/src/main/resources/bidder-config/escalax.yaml new file mode 100644 index 00000000000..8c6c44dbdea --- /dev/null +++ b/src/main/resources/bidder-config/escalax.yaml @@ -0,0 +1,17 @@ +adapters: + escalax: + endpoint: http://bidder_us.escalax.io/?partner={{.SourceId}}&token={{.AccountID}}&type=pbs + modifying-vast-xml-allowed: true + endpoint-compression: gzip + meta-info: + maintainer-email: connect@escalax.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/freewheelssp.yaml b/src/main/resources/bidder-config/freewheelssp.yaml index 5a5cc21f466..b198a837a9c 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -2,7 +2,7 @@ adapters: freewheelssp: endpoint: https://ads.stickyadstv.com/openrtb/dsp ortb-version: "2.6" - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: prebid-maintainer@freewheel.com app-media-types: diff --git a/src/main/resources/bidder-config/kargo.yaml b/src/main/resources/bidder-config/kargo.yaml index c4e3f78d8e8..56d5e9ea22d 100644 --- a/src/main/resources/bidder-config/kargo.yaml +++ b/src/main/resources/bidder-config/kargo.yaml @@ -3,7 +3,7 @@ adapters: endpoint: https://krk.kargo.com/api/v1/openrtb ortb-version: "2.6" endpoint-compression: gzip - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: kraken@kargo.com app-media-types: diff --git a/src/main/resources/bidder-config/sovrn.yaml b/src/main/resources/bidder-config/sovrn.yaml index d2d22c55c21..bad733681a6 100644 --- a/src/main/resources/bidder-config/sovrn.yaml +++ b/src/main/resources/bidder-config/sovrn.yaml @@ -1,7 +1,7 @@ adapters: sovrn: endpoint: http://ap.lijit.com/rtb/bid?src=prebid_server - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com app-media-types: diff --git a/src/main/resources/bidder-config/sovrnXsp.yaml b/src/main/resources/bidder-config/sovrnXsp.yaml index 706a06caafe..6a2a626e66f 100644 --- a/src/main/resources/bidder-config/sovrnXsp.yaml +++ b/src/main/resources/bidder-config/sovrnXsp.yaml @@ -2,7 +2,7 @@ adapters: sovrnXsp: endpoint: http://xsp.lijit.com/json/rtb/prebid/server endpoint-compression: gzip - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com app-media-types: diff --git a/src/main/resources/bidder-config/trafficgate.yaml b/src/main/resources/bidder-config/trafficgate.yaml index e4dd6b1fcd6..135d61e2fbe 100644 --- a/src/main/resources/bidder-config/trafficgate.yaml +++ b/src/main/resources/bidder-config/trafficgate.yaml @@ -1,7 +1,7 @@ adapters: trafficgate: endpoint: http://{{subdomain}}.bc-plugin.com/?c=o&m=rtb - modifyingVastXmlAllowed: true + modifying-vast-xml-allowed: true meta-info: maintainer-email: "support@bidscube.com" app-media-types: diff --git a/src/main/resources/static/bidder-params/escalax.json b/src/main/resources/static/bidder-params/escalax.json new file mode 100644 index 00000000000..68fda39c259 --- /dev/null +++ b/src/main/resources/static/bidder-params/escalax.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Escalax Adapter Params", + "description": "A schema which validates params accepted by the Escalax adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account id", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "description": "Source id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "sourceId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java b/src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java new file mode 100644 index 00000000000..34a74ff904a --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/escalax/EscalaxBidderTest.java @@ -0,0 +1,269 @@ +package org.prebid.server.bidder.escalax; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.escalax.ExtImpEscalax; + +import java.util.Arrays; +import java.util.List; +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.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER; +import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; +import static org.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class EscalaxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com?k={{AccountID}}&name={{SourceId}}"; + + private final EscalaxBidder target = new EscalaxBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new EscalaxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldRemoveOnlyFirstImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1"), + imp -> imp.id("impId2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(imps -> imps.getFirst()) + .extracting(Imp::getExt) + .containsOnlyNulls(); + + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(imps -> imps.get(1)) + .extracting(Imp::getExt) + .doesNotContainNull(); + } + + @Test + public void makeHttpRequestsShouldMakeSingleRequestForAllImps() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(2); + + assertThat(result.getValue()).hasSize(1) + .flatExtracting(HttpRequest::getImpIds) + .containsOnly("givenImp1", "givenImp2"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get(X_OPENRTB_VERSION_HEADER)) + .isEqualTo("2.5")) + .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)) + .isEqualTo("ua")) + .satisfies(headers -> assertThat(headers.getAll(X_FORWARDED_FOR_HEADER)) + .isEqualTo(List.of("ipv6", "ip"))); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnHttpRequestWithCorrectUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com?k=accountId&name=sourceId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(builder -> builder + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))))) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_input + && error.getMessage().startsWith("Error parsing escalaxExt - Cannot deserialize")); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseCanNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> actual = target.makeBids(httpCall, null); + + // then + assertThat(actual.getValue()).isEmpty(); + assertThat(actual.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseDoesNotHaveSeatBid() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> actual = target.makeBids(httpCall, null); + + // then + assertThat(actual.getValue()).isEmpty(); + assertThat(actual.getErrors()).containsExactly(badServerResponse("Empty SeatBid array")); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("1").mtype(1))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("1").build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("2").mtype(2))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("2").build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnBidsSuccessfully() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("4").mtype(4))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(4).impid("4").build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenImpTypeIsNotSupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("3").mtype(3))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsExactly(badServerResponse("unsupported MType 3")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) + .imp(Arrays.stream(impCustomizers).map(EscalaxBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpEscalax.of("sourceId", "accountId"))))) + .build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + + private String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + +} diff --git a/src/test/java/org/prebid/server/it/EscalaxTest.java b/src/test/java/org/prebid/server/it/EscalaxTest.java new file mode 100644 index 00000000000..30831d991e5 --- /dev/null +++ b/src/test/java/org/prebid/server/it/EscalaxTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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; +import static java.util.Collections.singletonList; + +public class EscalaxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromEscalax() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/escalax-exchange")) + .withQueryParam("k", equalTo("testAccountId")) + .withQueryParam("name", equalTo("testSourceId")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/escalax/test-escalax-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/escalax/test-escalax-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/escalax/test-auction-escalax-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/escalax/test-auction-escalax-response.json", response, + singletonList("escalax")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json new file mode 100644 index 00000000000..664693ffa74 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-request.json @@ -0,0 +1,27 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + }, + "ext": { + "escalax": { + "accountId": "testAccountId", + "sourceId": "testSourceId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json new file mode 100644 index 00000000000..0aa7a90e2d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "adm": "adm001", + "crid": "crid", + "w": 800, + "h": 600, + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + }, + "origbidcpm": 1.25 + } + } + ], + "seat": "escalax", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "escalax": "{{ escalax.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json new file mode 100644 index 00000000000..e0c6fddd7c7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-request.json @@ -0,0 +1,53 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "video": { + "mimes": [ + "video/mp4" + ], + "w": 800, + "h": 600 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json new file mode 100644 index 00000000000..b00165a1652 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-escalax-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 1.25, + "crid": "crid", + "adm": "adm001", + "h": 600, + "w": 800, + "mtype": 2 + } + ] + } + ], + "bidid": "bid001" +} 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 9369f0455b8..dd767fd8937 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -192,6 +192,8 @@ adapters.epom.endpoint=http://localhost:8090/epom-exchange adapters.epsilon.enabled=true adapters.epsilon.endpoint=http://localhost:8090/epsilon-exchange adapters.epsilon.generate-bid-id=false +adapters.escalax.enabled=true +adapters.escalax.endpoint=http://localhost:8090/escalax-exchange?k={{AccountID}}&name={{SourceId}} adapters.evolution.enabled=true adapters.evolution.endpoint=http://localhost:8090/evolution-exchange adapters.evtech.enabled=true From c6c4153f75f2b64750a0dab5f41efcaa37f9eb07 Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:27:37 +0300 Subject: [PATCH 054/170] Tests: Fix startup for `localstack` container (#3436) --- .../functional/testcontainers/Dependencies.groovy | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy index ef2575ea3ed..70c99a2a833 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy @@ -10,6 +10,7 @@ import org.testcontainers.lifecycle.Startables import org.testcontainers.utility.DockerImageName import static org.prebid.server.functional.util.SystemProperties.MOCKSERVER_VERSION +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3 class Dependencies { @@ -36,21 +37,21 @@ class Dependencies { static final NetworkServiceContainer networkServiceContainer = new NetworkServiceContainer(MOCKSERVER_VERSION) .withNetwork(network) - static final LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) - .withNetwork(Dependencies.network) - .withServices(LocalStackContainer.Service.S3) + static LocalStackContainer localStackContainer static void start() { if (IS_LAUNCH_CONTAINERS) { - Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]) - .join() + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:s3-latest")) + .withNetwork(network) + .withServices(S3) + Startables.deepStart([networkServiceContainer, mysqlContainer, localStackContainer]).join() } } static void stop() { if (IS_LAUNCH_CONTAINERS) { [networkServiceContainer, mysqlContainer, localStackContainer].parallelStream() - .forEach({ it.stop() }) + .forEach({ it.stop() }) } } From bfe348b810f82771f82057f21a30bb5280df2782 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:30:05 +0200 Subject: [PATCH 055/170] Bizzclick: Rename to Blasto (#3435) --- .../BlastoBidder.java} | 37 +++++++--------- .../ExtImpBlasto.java} | 4 +- ...guration.java => BlastoConfiguration.java} | 22 +++++----- .../resources/bidder-config/bizzclick.yaml | 15 ------- src/main/resources/bidder-config/blasto.yaml | 22 ++++++++++ .../{bizzclick.json => blasto.json} | 10 ++--- .../BlastoBidderTest.java} | 44 ++++--------------- .../{BizzclickTest.java => BlastoTest.java} | 19 ++++---- .../test-auction-blasto-request.json} | 5 +-- .../test-auction-blasto-response.json} | 4 +- .../test-blasto-bid-request.json} | 0 .../test-blasto-bid-response.json} | 0 .../server/it/test-application.properties | 4 +- 13 files changed, 80 insertions(+), 106 deletions(-) rename src/main/java/org/prebid/server/bidder/{bizzclick/BizzclickBidder.java => blasto/BlastoBidder.java} (80%) rename src/main/java/org/prebid/server/proto/openrtb/ext/request/{bizzclick/ExtImpBizzclick.java => blasto/ExtImpBlasto.java} (77%) rename src/main/java/org/prebid/server/spring/config/bidder/{BizzclickConfiguration.java => BlastoConfiguration.java} (59%) delete mode 100644 src/main/resources/bidder-config/bizzclick.yaml create mode 100644 src/main/resources/bidder-config/blasto.yaml rename src/main/resources/static/bidder-params/{bizzclick.json => blasto.json} (72%) rename src/test/java/org/prebid/server/bidder/{bizzclick/BizzclickBidderTest.java => blasto/BlastoBidderTest.java} (91%) rename src/test/java/org/prebid/server/it/{BizzclickTest.java => BlastoTest.java} (57%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-auction-bizzclick-request.json => blasto/test-auction-blasto-request.json} (75%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-auction-bizzclick-response.json => blasto/test-auction-blasto-response.json} (89%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-bizzclick-bid-request.json => blasto/test-blasto-bid-request.json} (100%) rename src/test/resources/org/prebid/server/it/openrtb2/{bizzclick/test-bizzclick-bid-response.json => blasto/test-blasto-bid-response.json} (100%) diff --git a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java similarity index 80% rename from src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java rename to src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java index 2fc8185e301..c317935419b 100644 --- a/src/main/java/org/prebid/server/bidder/bizzclick/BizzclickBidder.java +++ b/src/main/java/org/prebid/server/bidder/blasto/BlastoBidder.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.bizzclick; +package org.prebid.server.bidder.blasto; import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; @@ -9,7 +9,6 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -21,7 +20,7 @@ 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.bizzclick.ExtImpBizzclick; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -29,13 +28,12 @@ import java.util.List; import java.util.Objects; -public class BizzclickBidder implements Bidder { +public class BlastoBidder implements Bidder { - private static final TypeReference> BIZZCLICK_EXT_TYPE_REFERENCE = + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String DEFAULT_HOST = "us-e-node1"; - private static final String URL_HOST_MACRO = "{{Host}}"; + private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String DEFAULT_CURRENCY = "USD"; @@ -43,7 +41,7 @@ public class BizzclickBidder implements Bidder { private final String endpointUrl; private final JacksonMapper mapper; - public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { + public BlastoBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -51,23 +49,23 @@ public BizzclickBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { final List imps = request.getImp(); - final ExtImpBizzclick extImpBizzclick; + final ExtImpBlasto extImp; try { - extImpBizzclick = parseImpExt(imps.getFirst()); + extImp = parseImpExt(imps.getFirst()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } final List modifiedImps = imps.stream() - .map(BizzclickBidder::modifyImp) + .map(BlastoBidder::modifyImp) .toList(); - return Result.withValue(createHttpRequest(request, modifiedImps, extImpBizzclick)); + return Result.withValue(createHttpRequest(request, modifiedImps, extImp)); } - private ExtImpBizzclick parseImpExt(Imp imp) throws PreBidException { + private ExtImpBlasto parseImpExt(Imp imp) throws PreBidException { try { - return mapper.mapper().convertValue(imp.getExt(), BIZZCLICK_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { throw new PreBidException("ext.bidder not provided"); } @@ -77,7 +75,7 @@ private static Imp modifyImp(Imp imp) { return imp.toBuilder().ext(null).build(); } - private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBizzclick ext) { + private HttpRequest createHttpRequest(BidRequest request, List imps, ExtImpBlasto ext) { final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); return HttpRequest.builder() @@ -102,13 +100,10 @@ private static MultiMap headers(Device device) { return headers; } - private String buildEndpointUrl(ExtImpBizzclick ext) { - final String host = StringUtils.isBlank(ext.getHost()) ? DEFAULT_HOST : ext.getHost(); - final String sourceId = StringUtils.isBlank(ext.getSourceId()) ? ext.getPlacementId() : ext.getSourceId(); + private String buildEndpointUrl(ExtImpBlasto extImp) { return endpointUrl - .replace(URL_HOST_MACRO, HttpUtil.encodeUrl(host)) - .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(sourceId)) - .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(ext.getAccountId())); + .replace(URL_SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId())) + .replace(URL_ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId())); } @Override diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java similarity index 77% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java rename to src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java index dae1cd49c62..99413fa5e40 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bizzclick/ExtImpBizzclick.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blasto/ExtImpBlasto.java @@ -1,10 +1,10 @@ -package org.prebid.server.proto.openrtb.ext.request.bizzclick; +package org.prebid.server.proto.openrtb.ext.request.blasto; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; @Value(staticConstructor = "of") -public class ExtImpBizzclick { +public class ExtImpBlasto { @JsonProperty("host") String host; diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java similarity index 59% rename from src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java rename to src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java index c65702aa9fa..1c57db91aba 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/BizzclickConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlastoConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.bizzclick.BizzclickBidder; +import org.prebid.server.bidder.blasto.BlastoBidder; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -16,26 +16,26 @@ import jakarta.validation.constraints.NotBlank; @Configuration -@PropertySource(value = "classpath:/bidder-config/bizzclick.yaml", factory = YamlPropertySourceFactory.class) -public class BizzclickConfiguration { +@PropertySource(value = "classpath:/bidder-config/blasto.yaml", factory = YamlPropertySourceFactory.class) +public class BlastoConfiguration { - private static final String BIDDER_NAME = "bizzclick"; + private static final String BIDDER_NAME = "blasto"; - @Bean("bizzclickConfigurationProperties") - @ConfigurationProperties("adapters.bizzclick") + @Bean("blastoConfigurationProperties") + @ConfigurationProperties("adapters.blasto") BidderConfigurationProperties configurationProperties() { return new BidderConfigurationProperties(); } @Bean - BidderDeps bizzclickBidderDeps(BidderConfigurationProperties bizzclickConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + BidderDeps blastoBidderDeps(BidderConfigurationProperties blastoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(bizzclickConfigurationProperties) + .withConfig(blastoConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new BizzclickBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new BlastoBidder(config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/resources/bidder-config/bizzclick.yaml b/src/main/resources/bidder-config/bizzclick.yaml deleted file mode 100644 index f5037c1014a..00000000000 --- a/src/main/resources/bidder-config/bizzclick.yaml +++ /dev/null @@ -1,15 +0,0 @@ -adapters: - bizzclick: - endpoint: http://{{Host}}.bizzclick.com/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} - meta-info: - maintainer-email: support@bizzclick.com - app-media-types: - - banner - - video - - native - site-media-types: - - banner - - video - - native - supported-vendors: - vendor-id: 0 diff --git a/src/main/resources/bidder-config/blasto.yaml b/src/main/resources/bidder-config/blasto.yaml new file mode 100644 index 00000000000..d202f0acb2b --- /dev/null +++ b/src/main/resources/bidder-config/blasto.yaml @@ -0,0 +1,22 @@ +# Contact support@blasto.ai to connect with Blasto exchange. +# We have the following regional endpoint sub-domains: +# US East: t-us +# EU: t-eu +# APAC: t-apac +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +adapters: + blasto: + endpoint: http://t-us.blasto.ai/bid?rtb_seat_id={{SourceId}}&secret_key={{AccountID}} + endpoint-compression: gzip + meta-info: + maintainer-email: support@blasto.ai + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/bizzclick.json b/src/main/resources/static/bidder-params/blasto.json similarity index 72% rename from src/main/resources/static/bidder-params/bizzclick.json rename to src/main/resources/static/bidder-params/blasto.json index 879ab45314f..23109fb2421 100644 --- a/src/main/resources/static/bidder-params/bizzclick.json +++ b/src/main/resources/static/bidder-params/blasto.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Bizzclick Adapter Params", - "description": "A schema which validates params accepted by the Bizzclick adapter", + "title": "Blasto Adapter Params", + "description": "A schema which validates params accepted by the Blasto adapter", "type": "object", "properties": { "accountId": { @@ -9,14 +9,14 @@ "description": "Account id", "minLength": 1 }, - "placementId": { + "sourceId": { "type": "string", - "description": "PlacementId id", + "description": "Source id", "minLength": 1 } }, "required": [ "accountId", - "placementId" + "sourceId" ] } diff --git a/src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java b/src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java similarity index 91% rename from src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java rename to src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java index 283047ecaff..aa19d5b55e6 100644 --- a/src/test/java/org/prebid/server/bidder/bizzclick/BizzclickBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/blasto/BlastoBidderTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.bidder.bizzclick; +package org.prebid.server.bidder.blasto; import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; @@ -19,7 +19,7 @@ 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.bizzclick.ExtImpBizzclick; +import org.prebid.server.proto.openrtb.ext.request.blasto.ExtImpBlasto; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -34,19 +34,19 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.groups.Tuple.tuple; -public class BizzclickBidderTest extends VertxTest { +public class BlastoBidderTest extends VertxTest { - private static final String ENDPOINT = "https://{{Host}}/uri?source={{SourceId}}&account={{AccountID}}"; + private static final String ENDPOINT = "https://test.com/uri?source={{SourceId}}&account={{AccountID}}"; private static final String DEFAULT_HOST = "host"; private static final String DEFAULT_ACCOUNT_ID = "accountId"; private static final String DEFAULT_SOURCE_ID = "sourceId"; private static final String DEFAULT_PLACEMENT_ID = "placementId"; - private final BizzclickBidder target = new BizzclickBidder(ENDPOINT, jacksonMapper); + private final BlastoBidder target = new BlastoBidder(ENDPOINT, jacksonMapper); @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new BizzclickBidder("incorrect_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> new BlastoBidder("incorrect_url", jacksonMapper)); } @Test @@ -206,33 +206,7 @@ public void makeHttpRequestsShouldCreateSingleRequestWithExpectedUri() { // then assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsExactly( - String.format("https://%s/uri?source=%s&account=%s", - DEFAULT_HOST, - DEFAULT_SOURCE_ID, - DEFAULT_ACCOUNT_ID)); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldCreateSingleRequestWithExpectedAlternativeUri() { - // given - final String expectedDefaultHost = "us-e-node1"; - final BidRequest bidRequest = givenBidRequest( - givenImp(expectedDefaultHost, DEFAULT_ACCOUNT_ID, DEFAULT_PLACEMENT_ID, null) - ); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly( - String.format("https://%s/uri?source=%s&account=%s", - expectedDefaultHost, - DEFAULT_PLACEMENT_ID, - DEFAULT_ACCOUNT_ID)); + .containsExactly("https://test.com/uri?source=sourceId&account=accountId"); assertThat(result.getErrors()).isEmpty(); } @@ -448,7 +422,7 @@ private Imp givenImp(UnaryOperator impCustomizer) { } private Imp givenImp() { - final ExtPrebid ext = ExtPrebid.of(null, ExtImpBizzclick.of( + final ExtPrebid ext = ExtPrebid.of(null, ExtImpBlasto.of( DEFAULT_HOST, DEFAULT_ACCOUNT_ID, DEFAULT_PLACEMENT_ID, DEFAULT_SOURCE_ID )); return givenImp(imp -> imp.ext(mapper.valueToTree(ext))); @@ -456,7 +430,7 @@ private Imp givenImp() { private Imp givenImp(String host, String accountId, String placementId, String sourceId) { final ExtPrebid ext = ExtPrebid.of( - null, ExtImpBizzclick.of(host, accountId, placementId, sourceId) + null, ExtImpBlasto.of(host, accountId, placementId, sourceId) ); return givenImp(imp -> imp.ext(mapper.valueToTree(ext))); } diff --git a/src/test/java/org/prebid/server/it/BizzclickTest.java b/src/test/java/org/prebid/server/it/BlastoTest.java similarity index 57% rename from src/test/java/org/prebid/server/it/BizzclickTest.java rename to src/test/java/org/prebid/server/it/BlastoTest.java index 2ef4c68dffa..e26d75e6ca1 100644 --- a/src/test/java/org/prebid/server/it/BizzclickTest.java +++ b/src/test/java/org/prebid/server/it/BlastoTest.java @@ -14,24 +14,23 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static java.util.Collections.singletonList; -public class BizzclickTest extends IntegrationTest { +public class BlastoTest extends IntegrationTest { @Test - public void openrtb2AuctionShouldRespondWithBidsFromBizzclick() throws IOException, JSONException { + public void openrtb2AuctionShouldRespondWithBidsFromBlasto() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bizzclick-exchange")) - .withQueryParam("host", equalTo("host")) - .withQueryParam("source", equalTo("placementId")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/blasto-exchange")) + .withQueryParam("source", equalTo("sourceId")) .withQueryParam("account", equalTo("accountId")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/bizzclick/test-bizzclick-bid-request.json"))) - .willReturn(aResponse().withBody(jsonFrom("openrtb2/bizzclick/test-bizzclick-bid-response.json")))); + .withRequestBody(equalToJson(jsonFrom("openrtb2/blasto/test-blasto-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/blasto/test-blasto-bid-response.json")))); // when - final Response response = responseFor("openrtb2/bizzclick/test-auction-bizzclick-request.json", + final Response response = responseFor("openrtb2/blasto/test-auction-blasto-request.json", Endpoint.openrtb2_auction); // then - assertJsonEquals("openrtb2/bizzclick/test-auction-bizzclick-response.json", response, - singletonList("bizzclick")); + assertJsonEquals("openrtb2/blasto/test-auction-blasto-response.json", response, + singletonList("blasto")); } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json similarity index 75% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json index bfbeccf737f..8ee8e6865d7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-request.json @@ -8,10 +8,9 @@ "h": 250 }, "ext": { - "bizzclick": { - "host": "host", + "blasto": { "accountId": "accountId", - "placementId": "placementId" + "sourceId": "sourceId" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json similarity index 89% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json index d024a8f093b..9bf200e6d9d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-auction-bizzclick-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json @@ -22,14 +22,14 @@ } } ], - "seat": "bizzclick", + "seat": "blasto", "group": 0 } ], "cur": "USD", "ext": { "responsetimemillis": { - "bizzclick": "{{ bizzclick.response_time_ms }}" + "blasto": "{{ blasto.response_time_ms }}" }, "prebid": { "auctiontimestamp": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-request.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-request.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-bid-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/bizzclick/test-bizzclick-bid-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/blasto/test-blasto-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 dd767fd8937..7f5bea5cbd0 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -127,8 +127,8 @@ adapters.bidstack.enabled=true adapters.bidstack.endpoint=http://localhost:8090/bidstack-exchange adapters.bigoad.enabled=true adapters.bigoad.endpoint=http://localhost:8090/bigoad-exchange -adapters.bizzclick.enabled=true -adapters.bizzclick.endpoint=http://localhost:8090/bizzclick-exchange?host={{Host}}&source={{SourceId}}&account={{AccountID}} +adapters.blasto.enabled=true +adapters.blasto.endpoint=http://localhost:8090/blasto-exchange?source={{SourceId}}&account={{AccountID}} adapters.bliink.enabled=true adapters.bliink.endpoint=http://localhost:8090/bliink-exchange adapters.bluesea.enabled=true From a69e3be3e1bb9132e639ac001488807ba1483345 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:33:40 -0400 Subject: [PATCH 056/170] Bugfix: RemoteFileSyncer handling of error responses (#3440) --- .../server/execution/RemoteFileSyncer.java | 31 ++++- .../execution/RemoteFileSyncerTest.java | 108 ++++++++++++++++-- 2 files changed, 123 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java index 9a6416ba44c..b841bf8a136 100644 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java @@ -1,5 +1,6 @@ package org.prebid.server.execution; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; @@ -69,12 +70,14 @@ public RemoteFileSyncer(RemoteFileProcessor processor, getFileRequestOptions = new RequestOptions() .setMethod(HttpMethod.GET) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); isUpdateRequiredRequestOptions = new RequestOptions() .setMethod(HttpMethod.HEAD) .setTimeout(timeout) - .setAbsoluteURI(downloadUrl); + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); } private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { @@ -112,8 +115,7 @@ private Future deleteFile(String filePath) { private Future syncRemoteFile(RetryPolicy retryPolicy) { return fileSystem.open(tmpFilePath, new OpenOptions()) - .compose(tmpFile -> httpClient.request(getFileRequestOptions) - .compose(HttpClientRequest::send) + .compose(tmpFile -> sendHttpRequest(getFileRequestOptions) .compose(response -> response.pipeTo(tmpFile)) .onComplete(result -> tmpFile.close())) @@ -148,8 +150,7 @@ private void setUpDeferredUpdate() { } private void updateIfNeeded() { - httpClient.request(isUpdateRequiredRequestOptions) - .compose(HttpClientRequest::send) + sendHttpRequest(isUpdateRequiredRequestOptions) .compose(response -> fileSystem.exists(saveFilePath) .compose(exists -> exists ? isLengthChanged(response) @@ -161,6 +162,24 @@ private void updateIfNeeded() { }); } + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .compose(this::validateResponse); + } + + private Future validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + return Future.failedFuture(new PreBidException( + String.format("Got unexpected response from server with status code %s and message %s", + statusCode, + response.statusMessage()))); + } else { + return Future.succeededFuture(response); + } + } + private Future isLengthChanged(HttpClientResponse response) { final String contentLengthParameter = response.getHeader(HttpHeaders.CONTENT_LENGTH); return StringUtils.isNumeric(contentLengthParameter) && !contentLengthParameter.equals("0") diff --git a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java b/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java index 341c7764cb3..9acd719d30f 100644 --- a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java +++ b/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java @@ -1,5 +1,6 @@ package org.prebid.server.execution; +import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -182,6 +183,8 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); given(httpClientResponse.getHeader(HttpHeaders.CONTENT_LENGTH)) .willReturn("notnumber"); @@ -209,7 +212,10 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.failedFuture(new IllegalArgumentException("ERROR"))); @@ -240,7 +246,10 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.succeededFuture(fileProps)); @@ -274,8 +283,12 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.pipeTo(any())).willReturn(Future.succeededFuture()); - given(httpClientResponse.getHeader(any(CharSequence.class))).willReturn(FILE_SIZE.toString()); + given(httpClientResponse.pipeTo(any())) + .willReturn(Future.succeededFuture()); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); + given(httpClientResponse.getHeader(any(CharSequence.class))) + .willReturn(FILE_SIZE.toString()); given(fileSystem.props(anyString())) .willReturn(Future.succeededFuture(fileProps)); @@ -291,7 +304,8 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { given(fileSystem.move(anyString(), any(), any(CopyOptions.class))) .willReturn(Future.succeededFuture()); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -354,7 +368,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())) .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture(new RuntimeException()))); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); // when remoteFileSyncer.sync(); @@ -370,7 +385,8 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { @Test public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotSet() { // given - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(false)); @@ -382,6 +398,8 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(HttpResponseStatus.OK.code()); given(httpClientResponse.pipeTo(asyncFile)) .willReturn(Future.succeededFuture()); @@ -395,6 +413,7 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS verify(fileSystem).open(eq(TMP_FILE_PATH), any()); verify(httpClient).request(any()); verify(asyncFile).close(); + verify(httpClientResponse).statusCode(); verify(remoteFileProcessor).setDataPath(any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); @@ -419,8 +438,6 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { given(httpClient.request(any())) .willReturn(Future.succeededFuture(httpClientRequest)); given(httpClientRequest.send()) - .willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientResponse.pipeTo(asyncFile)) .willReturn(Future.failedFuture("Timeout")); // when @@ -429,6 +446,7 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { // then verify(vertx, times(RETRY_COUNT)).setTimer(eq(RETRY_INTERVAL), any()); verify(fileSystem, times(RETRY_COUNT + 1)).open(eq(TMP_FILE_PATH), any()); + verify(httpClientResponse, never()).pipeTo(any()); // Response handled verify(httpClient, times(RETRY_COUNT + 1)).request(any()); @@ -437,11 +455,81 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { verifyNoInteractions(remoteFileProcessor); } + @Test + public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() { + // given + given(fileSystem.exists(anyString())) + .willReturn(Future.succeededFuture(false)); + given(fileSystem.open(any(), any())) + .willReturn(Future.succeededFuture(asyncFile)); + given(fileSystem.move(anyString(), anyString(), any(CopyOptions.class))) + .willReturn(Future.succeededFuture()); + + given(httpClient.request(any())) + .willReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()) + .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(0); + + // when + remoteFileSyncer.sync(); + + // then + verify(fileSystem, times(1)).exists(eq(FILE_PATH)); + verify(fileSystem).open(eq(TMP_FILE_PATH), any()); + verify(fileSystem).delete(eq(TMP_FILE_PATH)); + verify(asyncFile).close(); + verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); + verify(httpClient).request(any()); + verify(httpClientResponse).statusCode(); + verify(httpClientResponse, never()).pipeTo(any()); + verify(remoteFileProcessor, never()).setDataPath(any()); + verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); + } + + @Test + public void syncShouldNotUpdateFileWhenServerRespondsWithNonOkStatusCode() { + // given + remoteFileSyncer = new RemoteFileSyncer( + remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); + + givenTriggerUpdate(); + + given(fileSystem.open(any(), any())) + .willReturn(Future.succeededFuture(asyncFile)); + given(fileSystem.move(anyString(), anyString(), any(CopyOptions.class))) + .willReturn(Future.succeededFuture()); + + given(httpClient.request(any())) + .willReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()) + .willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientResponse.statusCode()) + .willReturn(0); + + // when + remoteFileSyncer.sync(); + + // then + verify(fileSystem, times(1)).exists(eq(FILE_PATH)); + verify(fileSystem, never()).open(any(), any()); + verify(fileSystem, never()).delete(any()); + verify(fileSystem, never()).move(any(), any(), any(), any()); + verify(asyncFile, never()).close(); + verify(httpClient, times(1)).request(any()); + verify(httpClientResponse).statusCode(); + verify(httpClientResponse, never()).pipeTo(any()); + verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); + } + private void givenTriggerUpdate() { given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(true)); - given(remoteFileProcessor.setDataPath(anyString())).willReturn(Future.succeededFuture()); + given(remoteFileProcessor.setDataPath(anyString())) + .willReturn(Future.succeededFuture()); given(vertx.setPeriodic(eq(UPDATE_INTERVAL), any())) .willAnswer(withReturnObjectAndPassObjectToHandler(123L, 123L, 1)) From 8ecf1a48f6aec951f3b6b9e50f57519ed7d98df9 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:57:31 +0200 Subject: [PATCH 057/170] Core: Add `video.poddedupe` field (#3424) --- src/main/java/com/iab/openrtb/request/Video.java | 6 ++++++ .../server/functional/model/request/auction/Video.groovy | 2 ++ .../prebid/server/functional/tests/OrtbConverterSpec.groovy | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/main/java/com/iab/openrtb/request/Video.java b/src/main/java/com/iab/openrtb/request/Video.java index f967886bf89..369d576a3ac 100644 --- a/src/main/java/com/iab/openrtb/request/Video.java +++ b/src/main/java/com/iab/openrtb/request/Video.java @@ -254,6 +254,12 @@ public class Video { */ List companiontype; + /** + * Indicates pod deduplication settings that will be applied to bid responses. Refer to + * List: Pod Deduplication in AdCOM 1.0. + */ + List poddedupe; + /** * An array of objects (Section 3.2.35) * indicating the floor prices for video creatives of various durations that the buyer may bid with. diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index bc2ef7f5a5c..a70ee05eac3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -43,6 +43,8 @@ class Video { List companionad List api List companiontype + @JsonProperty("poddedupe") + List podDeduplication static Video getDefaultVideo() { new Video(mimes: ["video/mp4"], weight: 300, height: 200) diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 6ffaedca01e..7eb388cf202 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -561,6 +561,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber] } } @@ -584,6 +585,7 @@ class OrtbConverterSpec extends BaseSpec { mincpmpersec = PBSUtils.randomDecimal slotinpod = PBSUtils.randomNumber plcmt = PBSUtils.getRandomEnum(VideoPlcmtSubtype) + podDeduplication = [PBSUtils.randomNumber, PBSUtils.randomNumber] } } From 1afd0a1062fa97da7212cf30da55b4749cbf9807 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Tue, 17 Sep 2024 13:39:51 +0200 Subject: [PATCH 058/170] Adtonos: Add new adapter (#3446) --- .../server/bidder/adtonos/AdtonosBidder.java | 145 ++++++++++ .../ext/request/adtonos/ExtImpAdtonos.java | 11 + .../config/bidder/AdtonosConfiguration.java | 41 +++ src/main/resources/bidder-config/adtonos.yaml | 22 ++ .../static/bidder-params/adtonos.json | 15 + .../bidder/adtonos/AdtonosBidderTest.java | 271 ++++++++++++++++++ .../org/prebid/server/it/AdtonosTest.java | 35 +++ .../adtonos/test-adtonos-bid-request.json | 56 ++++ .../adtonos/test-adtonos-bid-response.json | 16 ++ .../adtonos/test-auction-adtonos-request.json | 23 ++ .../test-auction-adtonos-response.json | 34 +++ .../server/it/test-application.properties | 2 + 12 files changed, 671 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java create mode 100644 src/main/resources/bidder-config/adtonos.yaml create mode 100644 src/main/resources/static/bidder-params/adtonos.json create mode 100644 src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AdtonosTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json diff --git a/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java new file mode 100644 index 00000000000..f780a020732 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adtonos/AdtonosBidder.java @@ -0,0 +1,145 @@ +package org.prebid.server.bidder.adtonos; + +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; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.adtonos.ExtImpAdtonos; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class AdtonosBidder implements Bidder { + + private static final TypeReference> ADTONOS_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{PublisherId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdtonosBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpAdtonos impExt = parseImpExt(bidRequest.getImp().getFirst()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(impExt), mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpAdtonos parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ADTONOS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext.bidder for impression index 0. Error Infomation: " + e.getMessage()); + } + } + + private String makeUrl(ExtImpAdtonos extImp) { + return endpointUrl.replace(PUBLISHER_ID_MACRO, extImp.getSupplierId()); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse bidResponse; + try { + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + final List bids = extractBids(bidResponse, httpCall.getRequest().getPayload(), errors); + + return Result.of(bids, errors); + } + + private static List extractBids(BidResponse bidResponse, + BidRequest bidRequest, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), bidRequest, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, BidRequest bidRequest, List errors) { + try { + return BidderBid.of(bid, resolveBidType(bid, bidRequest.getImp()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType resolveBidType(Bid bid, List imps) throws PreBidException { + final Integer markupType = bid.getMtype(); + if (markupType != null) { + switch (markupType) { + case 1 -> { + return BidType.banner; + } + case 2 -> { + return BidType.video; + } + case 3 -> { + return BidType.audio; + } + case 4 -> { + return BidType.xNative; + } + } + } + + final String impId = bid.getImpid(); + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getVideo() != null) { + return BidType.video; + } + throw new PreBidException("Unsupported bidtype for bid: " + bid.getId()); + } + } + + throw new PreBidException("Failed to find impression: " + impId); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java new file mode 100644 index 00000000000..121d025f654 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adtonos/ExtImpAdtonos.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.adtonos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdtonos { + + @JsonProperty("supplierId") + String supplierId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java new file mode 100644 index 00000000000..8a86c88ac81 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdtonosConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adtonos.AdtonosBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/adtonos.yaml", factory = YamlPropertySourceFactory.class) +public class AdtonosConfiguration { + + private static final String BIDDER_NAME = "adtonos"; + + @Bean("adtonosConfigurationProperties") + @ConfigurationProperties("adapters.adtonos") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adtonosBidderDeps(BidderConfigurationProperties adtonosConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adtonosConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdtonosBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/adtonos.yaml b/src/main/resources/bidder-config/adtonos.yaml new file mode 100644 index 00000000000..e1a19fbc6eb --- /dev/null +++ b/src/main/resources/bidder-config/adtonos.yaml @@ -0,0 +1,22 @@ +adapters: + adtonos: + endpoint: https://exchange.adtonos.com/bid/{{PublisherId}} + geoscope: + - global + meta-info: + maintainer-email: support@adtonos.com + app-media-types: + - video + - audio + site-media-types: + - audio + dooh-media-types: + - audio + supported-vendors: + vendor-id: 682 + usersync: + cookie-family-name: adtonos + redirect: + url: https://play.adtonos.com/redir?to={{redirect_url}} + support-cors: false + uid-macro: '@UUID@' diff --git a/src/main/resources/static/bidder-params/adtonos.json b/src/main/resources/static/bidder-params/adtonos.json new file mode 100644 index 00000000000..b1ea833f1e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/adtonos.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdTonos Adapter Params", + "description": "A schema which validates params accepted by the AdTonos adapter", + "type": "object", + "properties": { + "supplierId": { + "type": "string", + "description": "ID of the supplier account in AdTonos platform" + } + }, + "required": [ + "supplierId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java b/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java new file mode 100644 index 00000000000..2d66b2f28ac --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/adtonos/AdtonosBidderTest.java @@ -0,0 +1,271 @@ +package org.prebid.server.bidder.adtonos; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adtonos.ExtImpAdtonos; + +import java.util.List; +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.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +public class AdtonosBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://randomurl.com?param={{PublisherId}}"; + + private final AdtonosBidder target = new AdtonosBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AdtonosBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateExpectedUrl() { + // given + final ExtImpAdtonos impExt = ExtImpAdtonos.of("publisherId"); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, impExt)))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://randomurl.com?param=publisherId"); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfMTypeIsOne() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(1).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfMTypeIsTwo() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(2).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidIfMTypeIsThree() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(3).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfMTypeIsFour() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(4).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBidsForAudioImpsIfMTypeMissed() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(impBuilder -> + impBuilder.audio(Audio.builder().build())))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnVideoBidsForVideoImpsIfMTypeMissed() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(impBuilder -> + impBuilder.video(Video.builder().build())))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorsForBidsThatDoesNotMatchSupportedMediaTypes() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(identity()))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().id("456").impid("123").build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("Unsupported bidtype for bid: 456"); + } + + @Test + public void makeBidsShouldReturnErrorsForBidsThatDoesNotContainMTypeAndImpMatch() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder().imp(singletonList(givenImp(identity()))).build(), + mapper.writeValueAsString(givenBidResponse(Bid.builder().impid("789").build(), + Bid.builder().id("123").mtype(1).build()))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().id("123").mtype(1).build(), banner, "USD")); + assertThat(result.getErrors()).hasSize(1) + .extracting(BidderError::getMessage) + .containsExactly("Failed to find impression: 789"); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder() + .imp(singletonList(impCustomizer.apply(Imp.builder().id("123")).build()))) + .build(); + } + + private static BidResponse givenBidResponse(Bid... bids) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(List.of(bids)) + .build())) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("123")) + .banner(Banner.builder().build()) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpAdtonos.of("testPubId")))) + .build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/AdtonosTest.java b/src/test/java/org/prebid/server/it/AdtonosTest.java new file mode 100644 index 00000000000..389edc02a5e --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdtonosTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class AdtonosTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheAdtonosBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtonos-exchange/testPublisherId")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/adtonos/test-adtonos-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/adtonos/test-adtonos-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/adtonos/test-auction-adtonos-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adtonos/test-auction-adtonos-response.json", response, + singletonList("adtonos")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json new file mode 100644 index 00000000000..ab7be17fc96 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "supplierId": "testPublisherId" + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json new file mode 100644 index 00000000000..c9191a06125 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-adtonos-bid-response.json @@ -0,0 +1,16 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json new file mode 100644 index 00000000000..8077266f37e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adtonos": { + "supplierId": "testPublisherId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json new file mode 100644 index 00000000000..e6795976a7f --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json @@ -0,0 +1,34 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "mtype": 1, + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "adtonos", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adtonos": "{{ adtonos.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} 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 7f5bea5cbd0..eebc53e085f 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -64,6 +64,8 @@ adapters.adtelligent.aliases.copper6.enabled=true adapters.adtelligent.aliases.copper6.endpoint=http://localhost:8090/copper6-exchange adapters.adtelligent.aliases.indicue.enabled=true adapters.adtelligent.aliases.indicue.endpoint=http://localhost:8090/indicue-exchange +adapters.adtonos.enabled=true +adapters.adtonos.endpoint=http://localhost:8090/adtonos-exchange/{{PublisherId}} adapters.adtrgtme.enabled=true adapters.adtrgtme.endpoint=http://localhost:8090/adtrgtme-exchange adapters.advangelists.enabled=true From b5c8d8ee912c2133d72d70effb2ef971d3b32257 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Tue, 17 Sep 2024 19:31:06 +0300 Subject: [PATCH 059/170] Github Actions: Add support for multiplatform docker image (x86-64, arm) (#3430) --- .github/workflows/docker-image-publish.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml index 286b03d03d3..39964eb69aa 100644 --- a/.github/workflows/docker-image-publish.yml +++ b/.github/workflows/docker-image-publish.yml @@ -55,11 +55,18 @@ jobs: with: images: ${{ matrix.package-name }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ${{ matrix.dockerfile-path }} push: true + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 1d5400b3ad75fc375fcfdc572558ae7ba6b3495b Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:31:44 +0200 Subject: [PATCH 060/170] Modules: Response Correction Module (#3409) --- extra/bundle/pom.xml | 5 + extra/modules/pb-response-correction/pom.xml | 15 + .../pb-response-correction/src/lombok.config | 1 + ...ResponseCorrectionModuleConfiguration.java | 37 ++ .../correction/core/CorrectionsProvider.java | 25 + .../core/config/model/AppVideoHtmlConfig.java | 15 + .../correction/core/config/model/Config.java | 13 + .../core/correction/Correction.java | 11 + .../core/correction/CorrectionProducer.java | 11 + .../appvideohtml/AppVideoHtmlCorrection.java | 137 +++++ .../AppVideoHtmlCorrectionProducer.java | 28 + ...orrectionAllProcessedBidResponsesHook.java | 104 ++++ .../v1/ResponseCorrectionModule.java | 32 ++ .../v1/model/InvocationResultImpl.java | 38 ++ .../core/CorrectionsProviderTest.java | 58 ++ .../AppVideoHtmlCorrectionProducerTest.java | 66 +++ .../AppVideoHtmlCorrectionTest.java | 197 +++++++ ...ctionAllProcessedBidResponsesHookTest.java | 118 ++++ extra/modules/pom.xml | 1 + .../server/functional/model/ModuleName.groovy | 5 +- .../model/config/AppVideoHtml.groovy | 15 + .../config/ModuleHookImplementation.groovy | 1 + .../model/config/Ortb2BlockingConfig.groovy | 2 - .../model/config/PbResponseCorrection.groovy | 13 + .../model/config/PbsModulesConfig.groovy | 1 + .../tests/module/ModuleBaseSpec.groovy | 7 + .../AnalyticsTagsModuleSpec.groovy | 3 +- .../ResponseCorrectionSpec.groovy | 507 ++++++++++++++++++ 28 files changed, 1461 insertions(+), 5 deletions(-) create mode 100644 extra/modules/pb-response-correction/pom.xml create mode 100644 extra/modules/pb-response-correction/src/lombok.config create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java create mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java create mode 100644 extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy rename src/test/groovy/org/prebid/server/functional/tests/module/{ => analyticstag}/AnalyticsTagsModuleSpec.groovy (99%) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index bd14db698fc..ad9d306f578 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -40,6 +40,11 @@ pb-richmedia-filter ${project.version} + + org.prebid.server.hooks.modules + pb-response-correction + ${project.version} + diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml new file mode 100644 index 00000000000..abf009733b2 --- /dev/null +++ b/extra/modules/pb-response-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.12.0-SNAPSHOT + + + pb-response-correction + + pb-response-correction + Response correction module + diff --git a/extra/modules/pb-response-correction/src/lombok.config b/extra/modules/pb-response-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-response-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..816cf122214 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.pb.response.correction.config; + +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrection; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrectionProducer; +import org.prebid.server.hooks.modules.pb.response.correction.v1.ResponseCorrectionModule; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + ResponseCorrectionModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class ResponseCorrectionModuleConfiguration { + + @Bean + AppVideoHtmlCorrectionProducer appVideoHtmlCorrectionProducer( + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new AppVideoHtmlCorrectionProducer( + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), logSamplingRate)); + } + + @Bean + CorrectionsProvider correctionsProvider(List correctionsProducers) { + return new CorrectionsProvider(correctionsProducers); + } + + @Bean + ResponseCorrectionModule responseCorrectionModule(CorrectionsProvider correctionsProvider) { + return new ResponseCorrectionModule(correctionsProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java new file mode 100644 index 00000000000..2afe514bf7d --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class CorrectionsProvider { + + private final List correctionsProducers; + + public CorrectionsProvider(List correctionsProducers) { + this.correctionsProducers = Objects.requireNonNull(correctionsProducers); + } + + public List corrections(Config config, BidRequest bidRequest) { + return correctionsProducers.stream() + .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) + .map(CorrectionProducer::produce) + .toList(); + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java new file mode 100644 index 00000000000..06b0990f149 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/AppVideoHtmlConfig.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AppVideoHtmlConfig { + + boolean enabled; + + @JsonProperty("excluded-bidders") + List excludedBidders; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java new file mode 100644 index 00000000000..17cd2453b16 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/config/model/Config.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Config { + + boolean enabled; + + @JsonProperty("app-video-html") + AppVideoHtmlConfig appVideoHtmlConfig; +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java new file mode 100644 index 00000000000..3f7abf1c5c5 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/Correction.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +import java.util.List; + +public interface Correction { + + List apply(Config config, List bidderResponses); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..6cd19836b96 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(); +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java new file mode 100644 index 00000000000..3df769e52cb --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java @@ -0,0 +1,137 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class AppVideoHtmlCorrection implements Correction { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(AppVideoHtmlCorrection.class)); + + private static final Pattern VAST_XML_PATTERN = Pattern.compile("<\\w*VAST\\w+", Pattern.CASE_INSENSITIVE); + private static final TypeReference> EXT_BID_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String NATIVE_ADM_MESSAGE = "Bid %s of bidder %s has an JSON ADM, that appears to be native"; + private static final String ADM_WITH_NO_ASSETS_MESSAGE = "Bid %s of bidder %s has a JSON ADM, but without assets"; + private static final String CHANGING_BID_MEDIA_TYPE_MESSAGE = "Bid %s of bidder %s: changing media type to banner"; + + private final ObjectMapper mapper; + private final double logSamplingRate; + + public AppVideoHtmlCorrection(ObjectMapper mapper, double logSamplingRate) { + this.mapper = mapper; + this.logSamplingRate = logSamplingRate; + } + + @Override + public List apply(Config config, List bidderResponses) { + final Collection excludedBidders = CollectionUtils.emptyIfNull( + config.getAppVideoHtmlConfig().getExcludedBidders()); + + return bidderResponses.stream() + .map(response -> modify(response, excludedBidders)) + .toList(); + } + + private BidderResponse modify(BidderResponse response, Collection excludedBidders) { + final String bidder = response.getBidder(); + if (excludedBidders.contains(bidder)) { + return response; + } + + final BidderSeatBid seatBid = response.getSeatBid(); + final List modifiedBids = seatBid.getBids().stream() + .map(bidderBid -> modifyBid(bidder, bidderBid)) + .toList(); + + return response.with(seatBid.with(modifiedBids)); + } + + private BidderBid modifyBid(String bidder, BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidId = bid.getId(); + final String adm = bid.getAdm(); + + if (adm == null || isVideoWithVastXml(bidderBid.getType(), adm) || hasNativeAdm(adm, bidId, bidder)) { + return bidderBid; + } + + conditionalLogger.warn(CHANGING_BID_MEDIA_TYPE_MESSAGE.formatted(bidId, bidder), logSamplingRate); + + final ExtBidPrebid prebid = parseExtBidPrebid(bid); + + final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .mediaType(BidType.video.getName()) + .build(); + + final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid) + .map(ExtBidPrebid::toBuilder) + .orElseGet(ExtBidPrebid::builder) + .meta(modifiedMeta) + .type(BidType.banner) + .build(); + + final ObjectNode modifiedBidExt = mapper.valueToTree(ExtPrebid.of(modifiedPrebid, null)); + + return bidderBid.toBuilder() + .type(BidType.banner) + .bid(bid.toBuilder().ext(modifiedBidExt).build()) + .build(); + } + + private boolean hasNativeAdm(String adm, String bidId, String bidder) { + final JsonNode admNode; + try { + admNode = mapper.readTree(adm); + } catch (JsonProcessingException e) { + return false; + } + + final boolean hasAssets = admNode.has("assets"); + final String warningMessage = hasAssets + ? NATIVE_ADM_MESSAGE.formatted(bidId, bidder) + : ADM_WITH_NO_ASSETS_MESSAGE.formatted(bidId, bidder); + + conditionalLogger.warn(warningMessage, logSamplingRate); + return hasAssets; + } + + private static boolean isVideoWithVastXml(BidType type, String adm) { + return type == BidType.video && VAST_XML_PATTERN.matcher(adm).matches(); + } + + private ExtBidPrebid parseExtBidPrebid(Bid bid) { + try { + return mapper.convertValue(bid.getExt(), EXT_BID_PREBID_TYPE_REFERENCE).getPrebid(); + } catch (Exception e) { + return null; + } + } + +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java new file mode 100644 index 00000000000..f7a05137bf0 --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducer.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +public class AppVideoHtmlCorrectionProducer implements CorrectionProducer { + + private final AppVideoHtmlCorrection correctionInstance; + + public AppVideoHtmlCorrectionProducer(AppVideoHtmlCorrection correction) { + this.correctionInstance = correction; + } + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final AppVideoHtmlConfig appVideoHtmlConfig = config.getAppVideoHtmlConfig(); + final boolean enabled = appVideoHtmlConfig != null && appVideoHtmlConfig.isEnabled(); + return enabled && bidRequest.getApp() != null; + } + + @Override + public Correction produce() { + return correctionInstance; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java new file mode 100644 index 00000000000..625c857a23e --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -0,0 +1,104 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.v1.model.InvocationResultImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; +import java.util.Objects; + +public class ResponseCorrectionAllProcessedBidResponsesHook implements AllProcessedBidResponsesHook { + + private static final String CODE = "pb-response-correction-all-processed-bid-responses-hook"; + + private final CorrectionsProvider correctionsProvider; + private final ObjectMapper mapper; + + public ResponseCorrectionAllProcessedBidResponsesHook(CorrectionsProvider correctionsProvider, ObjectMapper mapper) { + this.correctionsProvider = Objects.requireNonNull(correctionsProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AllProcessedBidResponsesPayload payload, + AuctionInvocationContext context) { + + final Config config; + try { + config = moduleConfig(context.accountConfig()); + } catch (PreBidException e) { + return failure(e.getMessage()); + } + + if (config == null || !config.isEnabled()) { + return noAction(); + } + + final BidRequest bidRequest = context.auctionContext().getBidRequest(); + + final List corrections = correctionsProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AllProcessedBidResponsesPayloadImpl.of( + applyCorrections(initialPayload.bidResponses(), config, corrections))) + .build(); + + return Future.succeededFuture(invocationResult); + } + + private Config moduleConfig(ObjectNode accountConfig) { + try { + return mapper.treeToValue(accountConfig, Config.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static List applyCorrections(List bidderResponses, Config config, List corrections) { + List result = bidderResponses; + for (Correction correction : corrections) { + result = correction.apply(config, result); + } + return result; + } + + private Future> failure(String message) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .action(InvocationAction.no_action) + .build()); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java new file mode 100644 index 00000000000..5ea3b583acd --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Collections; + +public class ResponseCorrectionModule implements Module { + + public static final String CODE = "pb-response-correction"; + + private final Collection> hooks; + + public ResponseCorrectionModule(CorrectionsProvider correctionsProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..1a39413583c --- /dev/null +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Object moduleContext; + + Tags analyticsTags; +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java new file mode 100644 index 00000000000..c5e7ac2d3f0 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core; + +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.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.CorrectionProducer; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class CorrectionsProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private CorrectionsProvider target; + + @BeforeEach + public void setUp() { + target = new CorrectionsProvider(singletonList(correctionProducer)); + } + + @Test + public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(false); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).isEmpty(); + } + + @Test + public void correctionsShouldReturnProducedCorrection() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(true); + + final Correction correction = mock(Correction.class); + given(correctionProducer.produce()).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java new file mode 100644 index 00000000000..15305d6bed2 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionProducerTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class AppVideoHtmlCorrectionProducerTest { + + private final AppVideoHtmlCorrection CORRECTION_INSTANCE = + new AppVideoHtmlCorrection(ObjectMapperProvider.mapper(), 0.1); + + private final AppVideoHtmlCorrectionProducer target = new AppVideoHtmlCorrectionProducer(CORRECTION_INSTANCE); + + @Test + public void produceShouldReturnCorrectionInstance() { + // when & then + assertThat(target.produce()).isSameAs(CORRECTION_INSTANCE); + } + + @Test + public void shouldProduceReturnFalseWhenAppVideoHtmlConfigIsDisabled() { + // given + final Config givenConfig = givenConfig(false); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnFalseWhenBidRequestIsNotAppRequest() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().site(Site.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isFalse(); + } + + @Test + public void shouldProduceReturnTrueWhenConfigIsEnabledAndBidRequestHasApp() { + // given + final Config givenConfig = givenConfig(true); + final BidRequest givenRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when + target.shouldProduce(givenConfig, givenRequest); + + // when & then + assertThat(target.shouldProduce(givenConfig, givenRequest)).isTrue(); + } + + private static Config givenConfig(boolean enabled) { + return Config.of(true, AppVideoHtmlConfig.of(enabled, null)); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java new file mode 100644 index 00000000000..537e79943cd --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java @@ -0,0 +1,197 @@ +package org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.AppVideoHtmlConfig; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AppVideoHtmlCorrectionTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + private final AppVideoHtmlCorrection target = new AppVideoHtmlCorrection(MAPPER, 0.1); + + @Test + public void applyShouldNotChangeBidResponsesFromExcludedBidders() { + // given + final Config givenConfig = givenConfig(List.of("bidderA", "bidderB")); + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", null, 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static Config givenConfig(List excludedBidders) { + return Config.of(true, AppVideoHtmlConfig.of(true, excludedBidders)); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenAdmIsNull() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + final BidderBid givenBid = givenBid(null, BidType.video); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of(List.of(givenBid)), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + private static BidderBid givenBid(String adm, BidType type) { + return givenBid(adm, type, null); + } + + private static BidderBid givenBid(String adm, BidType type, ObjectNode bidExt) { + final Bid bid = Bid.builder().adm(adm).ext(bidExt).build(); + return BidderBid.of(bid, type, "USD"); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidIsVideoAndHasVastXmlInAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid(" actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldNotChangeBidResponsesWhenBidHasNativeAdm() { + // given + final Config givenConfig = givenConfig(List.of("bidderA")); + + final List givenResponses = List.of( + BidderResponse.of("bidderA", null, 100), + BidderResponse.of("bidderB", BidderSeatBid.of( + List.of(givenBid("{\"field\":1,\"assets\":[{\"id\":2}]}", BidType.video))), 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + assertThat(actual).isEqualTo(givenResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsJsonButNotNative() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.video))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("{\"field\":1}", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + private static Config givenConfig() { + return Config.of(true, AppVideoHtmlConfig.of(true, null)); + } + + @Test + public void applyShouldChangeTypeToBannerAndAddMetaTypeVideoWhenAdmIsVastXmlAndTypeIsNotVideo() { + // given + final Config givenConfig = givenConfig(); + + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.xNative))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + + @Test + public void applyShouldChangeTypeToBannerAndOverwriteMetaTypeToVideoWhenAdmIsNotVastXmlAndTypeIsVideo() { + // given + final Config givenConfig = givenConfig(); + + final ExtBidPrebid givenPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("banner").build()) + .build(); + final ObjectNode givenBidExt = MAPPER.valueToTree(ExtPrebid.of(givenPrebid, null)); + final List givenResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.video, givenBidExt))), + 100)); + + // when + final List actual = target.apply(givenConfig, givenResponses); + + // then + final ExtBidPrebid expectedPrebid = ExtBidPrebid.builder() + .bidid("someId") + .meta(ExtBidPrebidMeta.builder().adapterCode("someCode").mediaType("video").build()) + .type(BidType.banner) + .build(); + final ObjectNode expectedBidExt = MAPPER.valueToTree(ExtPrebid.of(expectedPrebid, null)); + final List expectedResponses = List.of(BidderResponse.of( + "bidderA", + BidderSeatBid.of(List.of(givenBid("", BidType.banner, expectedBidExt))), + 100)); + + assertThat(actual).isEqualTo(expectedResponses); + } + +} diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java new file mode 100644 index 00000000000..0171f17cc04 --- /dev/null +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java @@ -0,0 +1,118 @@ +package org.prebid.server.hooks.modules.pb.response.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +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.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ResponseCorrectionAllProcessedBidResponsesHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private CorrectionsProvider correctionsProvider; + + private ResponseCorrectionAllProcessedBidResponsesHook target; + + @Mock + private AllProcessedBidResponsesPayload payload; + + @Mock(strictness = Mock.Strictness.LENIENT) + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(true, null))); + given(invocationContext.auctionContext()) + .willReturn(AuctionContext.builder().bidRequest(BidRequest.builder().build()).build()); + + target = new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, MAPPER); + } + + @Test + public void callShouldReturnFailedResultOnInvalidConfiguration() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList()))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type"); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionOnDisabledConfig() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.of(false, null))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { + // given + given(correctionsProvider.corrections(any(), any())).willReturn(emptyList()); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnUpdate() { + // given + final Correction correction = mock(Correction.class); + given(correctionsProvider.corrections(any(), any())).willReturn(singletonList(correction)); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + }); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 64def14267f..f9c7a337e5d 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -21,6 +21,7 @@ confiant-ad-quality pb-richmedia-filter fiftyone-devicedetection + pb-response-correction diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index eedb8412ba5..5efcdf40709 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -4,8 +4,9 @@ import com.fasterxml.jackson.annotation.JsonValue enum ModuleName { - PB_RICHMEDIA_FILTER('pb-richmedia-filter'), - ORTB2_BLOCKING('ortb2-blocking') + PB_RICHMEDIA_FILTER("pb-richmedia-filter"), + PB_RESPONSE_CORRECTION ("pb-response-correction"), + ORTB2_BLOCKING("ortb2-blocking") @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy new file mode 100644 index 00000000000..6486e292ed5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AppVideoHtml.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AppVideoHtml { + + Boolean enabled + List excludedBidders +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 11173093f85..0d8333b3375 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -7,6 +7,7 @@ import org.prebid.server.functional.model.ModuleName enum ModuleHookImplementation { PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"), + RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses-hook"), ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy index fbbe08089fc..6b5b8f4adb0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -1,7 +1,5 @@ package org.prebid.server.functional.model.config -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy new file mode 100644 index 00000000000..46af75deac6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbResponseCorrection.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class PbResponseCorrection { + + Boolean enabled + AppVideoHtml appVideoHtml +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index 74a6ddab94d..f9121ae0b3a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -11,4 +11,5 @@ class PbsModulesConfig { RichmediaFilter pbRichmediaFilter Ortb2BlockingConfig ortb2Blocking + PbResponseCorrection pbResponseCorrection } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 4a342e602a4..19cb2cd53de 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -5,6 +5,7 @@ import org.prebid.server.functional.model.config.ExecutionPlan import org.prebid.server.functional.tests.BaseSpec import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES @@ -22,6 +23,12 @@ class ModuleBaseSpec extends BaseSpec { repository.removeAllDatabaseData() } + protected static Map getResponseCorrectionConfig(Endpoint endpoint = OPENRTB2_AUCTION) { + ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled" : true, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RESPONSE_CORRECTION, [ALL_PROCESSED_BID_RESPONSES]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getRichMediaFilterSettings(String scriptPattern, boolean filterMraidEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy similarity index 99% rename from src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy rename to src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy index 511c2101eee..8a99628b70c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/AnalyticsTagsModuleSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy @@ -1,4 +1,4 @@ -package org.prebid.server.functional.tests.module +package org.prebid.server.functional.tests.module.analyticstag import org.prebid.server.functional.model.config.AccountAnalyticsConfig import org.prebid.server.functional.model.config.AccountConfig @@ -16,6 +16,7 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ModuleActivityName import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy new file mode 100644 index 00000000000..2c6c4dfd146 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy @@ -0,0 +1,507 @@ +package org.prebid.server.functional.tests.module.requestcorrenction + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AppVideoHtml +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.response.auction.Adm +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.Meta +import org.prebid.server.functional.model.response.auction.Prebid +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultVideoRequest +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class ResponseCorrectionSpec extends ModuleBaseSpec { + + private final PrebidServerService pbsServiceWithResponseCorrectionModule = pbsServiceFactory.getService( + ["adapter-defaults.modifying-vast-xml-allowed": "false", + "adapters.generic.modifying-vast-xml-allowed": "false"] + + responseCorrectionConfig) + + def "PBS shouldn't modify response when in account correction module disabled"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest, responseCorrectionEnabled, appVideoHtmlEnabled) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + responseCorrectionEnabled | appVideoHtmlEnabled + false | true + true | false + false | false + } + + def "PBS shouldn't modify response with adm obj when request includes #distributionChannel distribution channel"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request video imp" + def bidRequest = getDefaultVideoRequest(distributionChannel) + + and: "Set bidder response with adm obj" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + distributionChannel << [SITE, DOOH] + } + + def "PBS shouldn't modify response for excluded bidder when bidder specified in config"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(VIDEO) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module and excluded bidders" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest).tap { + config.hooks.modules.pbResponseCorrection.appVideoHtml.excludedBidders = [GENERIC] + } + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response and emit warning when requested video impression respond with adm without VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response without adm obj when request includes #mediaType media type"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [mediaType] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType << [BANNER, AUDIO] + } + + def "PBS shouldn't modify response and emit logs when requested impression with native and adm value is asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection[0].contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + assert responseCorrection.size() == 1 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [NATIVE] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with empty adm"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(null) + seatbid[0].bid[0].nurl = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS shouldn't modify response when requested video impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase("<${PBSUtils.randomString}VAST${PBSUtils.randomString}")) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + assert getLogsByText(logsByTime, bidResponse.seatbid[0].bid[0].id).size() == 0 + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS should modify response when requested #mediaType impression respond with adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase("<${PBSUtils.randomString}VAST${PBSUtils.randomString}")) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + mediaType << [BANNER, AUDIO, NATIVE] + } + + def "PBS shouldn't modify response meta.mediaType to video and emit logs when requested impression with video and adm obj with asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and audio imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has an JSON ADM, that appears to be native" as String) + } + + and: "Response should contain seatBib" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [VIDEO] + + and: "Response shouldn't contain media type for prebid meta" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.mediaType?.flatten()?.size() + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + def "PBS should modify meta.mediaType and type for original response and also emit logs when response contains native meta.mediaType and adm without asset"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and #mediaType imp" + def bidRequest = getDefaultBidRequest(APP).tap { + imp[0] = Imp.getDefaultImpression(NATIVE) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm() + seatbid[0].bid[0].ext = new BidExt(prebid: new Prebid(meta: new Meta(mediaType: NATIVE))) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 2 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic has a JSON ADM, but without assets" as String) + } + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should media type for prebid meta" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + } + + private static Account accountConfigWithResponseCorrectionModule(BidRequest bidRequest, Boolean enabledResponseCorrection = true, Boolean enabledAppVideoHtml = true) { + def modulesConfig = new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection( + enabled: enabledResponseCorrection, appVideoHtml: new AppVideoHtml(enabled: enabledAppVideoHtml))) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + } +} From dd5b29b86d3b73b2287143a6c5e087ac8a96f395 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:56:49 +0200 Subject: [PATCH 061/170] Core: Add hb_env=amp for Amp Requests (#3433) --- .../server/auction/BidResponseCreator.java | 31 +++++++------ .../auction/TargetingKeywordsCreator.java | 18 +++----- .../functional/tests/TargetingSpec.groovy | 25 ++++++++++- .../auction/BidResponseCreatorTest.java | 43 ++++++++++++++++++- .../auction/TargetingKeywordsCreatorTest.java | 36 ++++++++-------- .../server/it/amp/test-amp-response.json | 3 ++ 6 files changed, 109 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index b843b7f07e4..567f73e63de 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -122,6 +122,8 @@ public class BidResponseCreator { private static final Integer MAX_TARGETING_KEY_LENGTH = 11; private static final String DEFAULT_TARGETING_KEY_PREFIX = "hb"; public static final String DEFAULT_DEBUG_KEY = "prebid"; + private static final String TARGETING_ENV_APP_VALUE = "mobile-app"; + private static final String TARGETING_ENV_AMP_VALUE = "amp"; private final CoreCacheService coreCacheService; private final BidderCatalog bidderCatalog; @@ -1325,13 +1327,11 @@ private Bid toBid(BidInfo bidInfo, final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; - final boolean isApp = bidRequest.getApp() != null; - final Map targetingKeywords; final String bidderCode = targetingInfo.getBidderCode(); if (shouldIncludeTargetingInResponse(targeting, bidInfo.getTargetingInfo())) { final TargetingKeywordsCreator keywordsCreator = resolveKeywordsCreator( - bidType, targeting, isApp, bidRequest, account, bidWarnings); + bidType, targeting, bidRequest, account, bidWarnings); final boolean isWinningBid = targetingInfo.isWinningBid(); final String categoryDuration = bidInfo.getCategory(); @@ -1552,16 +1552,15 @@ private Events createEvents(String bidder, private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { final Map keywordsCreatorByBidType = - keywordsCreatorByBidType(targeting, isApp, bidRequest, account, bidWarnings); + keywordsCreatorByBidType(targeting, bidRequest, account, bidWarnings); return keywordsCreatorByBidType.getOrDefault( - bidType, keywordsCreator(targeting, isApp, bidRequest, account, bidWarnings)); + bidType, keywordsCreator(targeting, bidRequest, account, bidWarnings)); } /** @@ -1569,7 +1568,6 @@ private TargetingKeywordsCreator resolveKeywordsCreator(BidType bidType, * instance if it is present. */ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1577,7 +1575,7 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, final JsonNode priceGranularityNode = targeting.getPricegranularity(); return priceGranularityNode == null || priceGranularityNode.isNull() ? null - : createKeywordsCreator(targeting, isApp, priceGranularityNode, bidRequest, account, bidWarnings); + : createKeywordsCreator(targeting, priceGranularityNode, bidRequest, account, bidWarnings); } /** @@ -1586,7 +1584,6 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, */ private Map keywordsCreatorByBidType( ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1602,21 +1599,21 @@ private Map keywordsCreatorByBidType( final boolean isBannerNull = banner == null || banner.isNull(); if (!isBannerNull) { result.put( - BidType.banner, createKeywordsCreator(targeting, isApp, banner, bidRequest, account, bidWarnings)); + BidType.banner, createKeywordsCreator(targeting, banner, bidRequest, account, bidWarnings)); } final ObjectNode video = mediaTypePriceGranularity.getVideo(); final boolean isVideoNull = video == null || video.isNull(); if (!isVideoNull) { result.put( - BidType.video, createKeywordsCreator(targeting, isApp, video, bidRequest, account, bidWarnings)); + BidType.video, createKeywordsCreator(targeting, video, bidRequest, account, bidWarnings)); } final ObjectNode xNative = mediaTypePriceGranularity.getXNative(); final boolean isNativeNull = xNative == null || xNative.isNull(); if (!isNativeNull) { result.put( - BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, account, bidWarnings) + BidType.xNative, createKeywordsCreator(targeting, xNative, bidRequest, account, bidWarnings) ); } @@ -1624,7 +1621,6 @@ BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, ac } private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targeting, - boolean isApp, JsonNode priceGranularity, BidRequest bidRequest, Account account, @@ -1632,13 +1628,20 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe final int resolvedTruncateAttrChars = resolveTruncateAttrChars(targeting, account); final String resolveKeyPrefix = resolveAndValidateKeyPrefix( bidRequest, account, resolvedTruncateAttrChars, bidWarnings); + + final String env = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAmp) + .map(ignored -> TARGETING_ENV_AMP_VALUE) + .orElse(bidRequest.getApp() == null ? null : TARGETING_ENV_APP_VALUE); + return TargetingKeywordsCreator.create( parsePriceGranularity(priceGranularity), BooleanUtils.toBoolean(targeting.getIncludewinners()), BooleanUtils.toBoolean(targeting.getIncludebidderkeys()), BooleanUtils.toBoolean(targeting.getAlwaysincludedeals()), BooleanUtils.isTrue(targeting.getIncludeformat()), - isApp, + env, resolvedTruncateAttrChars, cacheHost, cachePath, diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 9472f734336..2896e153adf 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -32,10 +32,6 @@ public class TargetingKeywordsCreator { * It will exist only if the incoming bidRequest defiend request.app instead of request.site. */ private static final String ENV_KEY = "_env"; - /** - * Used as a value for ENV_KEY. - */ - private static final String ENV_APP_VALUE = "mobile-app"; /** * Name of the Bidder. For example, "appnexus" or "rubicon". */ @@ -87,7 +83,7 @@ public class TargetingKeywordsCreator { private final boolean includeBidderKeys; private final boolean alwaysIncludeDeals; private final boolean includeFormat; - private final boolean isApp; + private final String env; private final int truncateAttrChars; private final String cacheHost; private final String cachePath; @@ -99,7 +95,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -111,7 +107,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, this.includeBidderKeys = includeBidderKeys; this.alwaysIncludeDeals = alwaysIncludeDeals; this.includeFormat = includeFormat; - this.isApp = isApp; + this.env = env; this.truncateAttrChars = truncateAttrChars; this.cacheHost = cacheHost; this.cachePath = cachePath; @@ -127,7 +123,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul boolean includeBidderKeys, boolean alwaysIncludeDeals, boolean includeFormat, - boolean isApp, + String env, int truncateAttrChars, String cacheHost, String cachePath, @@ -139,7 +135,7 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul includeBidderKeys, alwaysIncludeDeals, includeFormat, - isApp, + env, truncateAttrChars, cacheHost, cachePath, @@ -230,8 +226,8 @@ private Map makeFor(String bidder, if (StringUtils.isNotBlank(dealId)) { keywordMap.put(this.keyPrefix + DEAL_KEY, dealId); } - if (isApp) { - keywordMap.put(this.keyPrefix + ENV_KEY, ENV_APP_VALUE); + if (env != null) { + keywordMap.put(this.keyPrefix + ENV_KEY, env); } if (StringUtils.isNotBlank(categoryDuration)) { keywordMap.put(this.keyPrefix + CATEGORY_DURATION_KEY, categoryDuration); diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy index 6005a232262..67ff7907901 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -30,10 +30,12 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor class TargetingSpec extends BaseSpec { private static final Integer TARGETING_PARAM_NAME_MAX_LENGTH = 20 + private static final Integer TARGETING_KEYS_SIZE = 14 private static final Integer MAX_AMP_TARGETING_TRUNCATION_LENGTH = 11 private static final String DEFAULT_TARGETING_PREFIX = "hb" private static final Integer TARGETING_PREFIX_LENGTH = 11 private static final Integer MAX_TRUNCATE_ATTR_CHARS = 255 + private static final String HB_ENV_AMP = "amp" def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -668,7 +670,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain default targeting prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 + assert targeting.size() == TARGETING_KEYS_SIZE assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } @@ -694,7 +696,7 @@ class TargetingSpec extends BaseSpec { then: "Amp response should contain targeting response with custom prefix" def targeting = ampResponse.targeting - assert targeting.size() == 12 + assert targeting.size() == TARGETING_KEYS_SIZE assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } } @@ -1100,6 +1102,25 @@ class TargetingSpec extends BaseSpec { assert ampData.secondUnknownField == secondUnknownValue } + def "PBS amp should always send hb_env=amp when stored request does not contain app"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default bid request" + def ampStoredRequest = BidRequest.defaultBidRequest + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Amp response should contain amp hb_env" + def targeting = ampResponse.targeting + assert targeting["hb_env"] == HB_ENV_AMP + } + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 674a3fcf245..17de10e38b5 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -82,6 +82,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAmp; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; @@ -1551,7 +1552,7 @@ public void shouldPopulateTargetingKeywords() { final AuctionContext auctionContext = givenAuctionContext( givenBidRequest( - identity(), + request -> request.app(App.builder().build()), extBuilder -> extBuilder.targeting(givenTargeting()), givenImp()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); @@ -1569,7 +1570,45 @@ public void shouldPopulateTargetingKeywords() { tuple("hb_pb", "5.00"), tuple("hb_pb_bidder1", "5.00"), tuple("hb_bidder", "bidder1"), - tuple("hb_bidder_bidder1", "bidder1")); + tuple("hb_bidder_bidder1", "bidder1"), + tuple("hb_env", "mobile-app"), + tuple("hb_env_bidder1", "mobile-app")); + + verify(coreCacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); + } + + @Test + public void shouldPopulateTargetingKeywordsForAmpRequest() { + // given + final Bid bid = Bid.builder().id("bidId1").price(BigDecimal.valueOf(5.67)).impid(IMP_ID).build(); + final List bidderResponses = singletonList(BidderResponse.of("bidder1", + givenSeatBid(BidderBid.of(bid, banner, "USD")), 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest( + request -> request.app(App.builder().build()), + extBuilder -> extBuilder + .targeting(givenTargeting()) + .amp(ExtRequestPrebidAmp.of(Map.of("key", "value"))), + givenImp()), + contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); + + // when + final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); + + // then + assertThat(bidResponse.getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1) + .extracting(extractedBid -> toExtBidPrebid(extractedBid.getExt()).getTargeting()) + .flatExtracting(Map::entrySet) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("hb_pb", "5.00"), + tuple("hb_pb_bidder1", "5.00"), + tuple("hb_bidder", "bidder1"), + tuple("hb_bidder_bidder1", "bidder1"), + tuple("hb_env", "amp"), + tuple("hb_env_bidder1", "amp")); verify(coreCacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); } diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index f63b6d32772..879bd7873c2 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -39,7 +39,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -70,7 +70,7 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -105,7 +105,7 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { true, false, true, - false, + null, 0, null, null, @@ -148,7 +148,7 @@ public void shouldIncludeFormatOpenrtb() { true, false, true, - false, + null, 0, null, null, @@ -174,7 +174,7 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { true, false, false, - false, + null, 0, null, null, @@ -201,7 +201,7 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { true, false, false, - true, + "mobile-app", 0, null, null, @@ -229,7 +229,7 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { true, false, false, - false, + null, 0, null, null, @@ -255,7 +255,7 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { true, false, false, - false, + null, 0, null, null, @@ -281,7 +281,7 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() false, false, false, - false, + null, 0, null, null, @@ -307,7 +307,7 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { true, false, false, - false, + null, 0, null, null, @@ -333,7 +333,7 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() true, false, false, - false, + null, 20, null, null, @@ -360,7 +360,7 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar false, false, false, - false, + null, 7, null, null, @@ -387,7 +387,7 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { true, false, true, - true, + "mobile-app", 6, null, null, @@ -415,7 +415,7 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() true, false, false, - false, + null, 0, null, null, @@ -448,7 +448,7 @@ public void shouldTruncateKeysFromResolver() { true, false, false, - false, + null, 20, null, null, @@ -480,7 +480,7 @@ public void shouldIncludeKeywordsFromResolver() { true, false, false, - false, + null, 0, null, null, @@ -506,7 +506,7 @@ public void shouldIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsTrue() { false, true, false, - false, + null, 0, null, null, @@ -532,7 +532,7 @@ public void shouldNotIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsFalse() { false, false, false, - false, + null, 0, null, null, diff --git a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json index fcac32fb76b..92abd79998c 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-amp-response.json +++ b/src/test/resources/org/prebid/server/it/amp/test-amp-response.json @@ -12,6 +12,9 @@ "hb_cache_id": "fea00992-651c-44c8-b16a-b9af99fdf2dd", "hb_bidder_generic": "generic", "hb_size_genericAlias": "300x250", + "hb_env": "amp", + "hb_env_generic": "amp", + "hb_env_genericAlias": "amp", "hb_cache_host": "{{ cache.host }}", "hb_cache_path": "{{ cache.path }}", "hb_cache_host_generic": "{{ cache.host }}", From f588358e373d9ab669d75458ba56ed8deb9b26f8 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Thu, 19 Sep 2024 12:11:56 +0200 Subject: [PATCH 062/170] Tests: Temporary disable not-stable test (#3450) --- src/test/java/org/prebid/server/it/PriceFloorsTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java index b32be1580d2..3dd300524c0 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.prebid.server.model.Endpoint; @@ -30,6 +31,8 @@ import static org.prebid.server.util.IntegrationTestsUtil.jsonFrom; import static org.prebid.server.util.IntegrationTestsUtil.responseFor; +// TODO: Investigate the root cause of unstable behavior in this class and remove the disabled state once resolved. +@Disabled @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @TestPropertySource( locations = {"test-application.properties"}, From e5a16ad150caca5ed2773dbc09755a1728e3fe03 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 20 Sep 2024 15:15:04 +0200 Subject: [PATCH 063/170] Prebid Server prepare release 3.12.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index ad9d306f578..920359ff5d5 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.12.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 7e3679cabb6..4c01162b25b 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.12.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 713641e0442..8a791ba9ef7 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.12.0 fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 4a07feabcd8..ff2a5cc40f2 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.12.0 ortb2-blocking diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index abf009733b2..9ff1d63d97d 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.12.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 97f3159d5cd..9daf20ae03e 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0-SNAPSHOT + 3.12.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index f9c7a337e5d..d546fbf9559 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.12.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 3364584f818..124da1b7da8 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.12.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.12.0 diff --git a/pom.xml b/pom.xml index f58a7184f2f..08b2950da58 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0-SNAPSHOT + 3.12.0 extra/pom.xml From 81c7b9da5081126e4d2da07528784d576f09f4ea Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 20 Sep 2024 15:15:04 +0200 Subject: [PATCH 064/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 920359ff5d5..33a26a74d4b 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0 + 3.13.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 4c01162b25b..e82a3d761ca 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0 + 3.13.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 8a791ba9ef7..4a4fcfa80da 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0 + 3.13.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index ff2a5cc40f2..6eac58c5ba0 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0 + 3.13.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 9ff1d63d97d..a84008ae0d4 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0 + 3.13.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 9daf20ae03e..4bf4b87f902 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.12.0 + 3.13.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index d546fbf9559..b7eac66e702 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0 + 3.13.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 124da1b7da8..2049380670c 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.12.0 + 3.13.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.12.0 + HEAD diff --git a/pom.xml b/pom.xml index 08b2950da58..5d8987c87bd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.12.0 + 3.13.0-SNAPSHOT extra/pom.xml From f94b1d494306b45f23c8ae76dbacf5524549da5b Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Mon, 23 Sep 2024 05:40:04 -0400 Subject: [PATCH 065/170] Core: Make Response correction module bean name more explicit (#3454) --- .../config/ResponseCorrectionModuleConfiguration.java | 10 +++++----- ...sProvider.java => ResponseCorrectionProvider.java} | 10 +++++----- ...esponseCorrectionAllProcessedBidResponsesHook.java | 11 ++++++----- .../correction/v1/ResponseCorrectionModule.java | 6 +++--- ...rTest.java => ResponseCorrectionProviderTest.java} | 6 +++--- ...nseCorrectionAllProcessedBidResponsesHookTest.java | 10 +++++----- 6 files changed, 27 insertions(+), 26 deletions(-) rename extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/{CorrectionsProvider.java => ResponseCorrectionProvider.java} (69%) rename extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/{CorrectionsProviderTest.java => ResponseCorrectionProviderTest.java} (90%) diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java index 816cf122214..e119bb59703 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/config/ResponseCorrectionModuleConfiguration.java @@ -4,7 +4,7 @@ import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrection; import org.prebid.server.hooks.modules.pb.response.correction.core.correction.appvideohtml.AppVideoHtmlCorrectionProducer; import org.prebid.server.hooks.modules.pb.response.correction.v1.ResponseCorrectionModule; -import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; import org.prebid.server.json.ObjectMapperProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -26,12 +26,12 @@ AppVideoHtmlCorrectionProducer appVideoHtmlCorrectionProducer( } @Bean - CorrectionsProvider correctionsProvider(List correctionsProducers) { - return new CorrectionsProvider(correctionsProducers); + ResponseCorrectionProvider responseCorrectionProvider(List correctionProducers) { + return new ResponseCorrectionProvider(correctionProducers); } @Bean - ResponseCorrectionModule responseCorrectionModule(CorrectionsProvider correctionsProvider) { - return new ResponseCorrectionModule(correctionsProvider, ObjectMapperProvider.mapper()); + ResponseCorrectionModule responseCorrectionModule(ResponseCorrectionProvider responseCorrectionProvider) { + return new ResponseCorrectionModule(responseCorrectionProvider, ObjectMapperProvider.mapper()); } } diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java similarity index 69% rename from extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java rename to extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java index 2afe514bf7d..9bdf2ceea8c 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProvider.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProvider.java @@ -8,16 +8,16 @@ import java.util.List; import java.util.Objects; -public class CorrectionsProvider { +public class ResponseCorrectionProvider { - private final List correctionsProducers; + private final List correctionProducers; - public CorrectionsProvider(List correctionsProducers) { - this.correctionsProducers = Objects.requireNonNull(correctionsProducers); + public ResponseCorrectionProvider(List correctionProducers) { + this.correctionProducers = Objects.requireNonNull(correctionProducers); } public List corrections(Config config, BidRequest bidRequest) { - return correctionsProducers.stream() + return correctionProducers.stream() .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) .map(CorrectionProducer::produce) .toList(); diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java index 625c857a23e..cea7a80b131 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -8,10 +8,10 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.exception.PreBidException; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; import org.prebid.server.hooks.modules.pb.response.correction.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -26,11 +26,12 @@ public class ResponseCorrectionAllProcessedBidResponsesHook implements AllProces private static final String CODE = "pb-response-correction-all-processed-bid-responses-hook"; - private final CorrectionsProvider correctionsProvider; + private final ResponseCorrectionProvider responseCorrectionProvider; private final ObjectMapper mapper; - public ResponseCorrectionAllProcessedBidResponsesHook(CorrectionsProvider correctionsProvider, ObjectMapper mapper) { - this.correctionsProvider = Objects.requireNonNull(correctionsProvider); + public ResponseCorrectionAllProcessedBidResponsesHook(ResponseCorrectionProvider responseCorrectionProvider, + ObjectMapper mapper) { + this.responseCorrectionProvider = Objects.requireNonNull(responseCorrectionProvider); this.mapper = Objects.requireNonNull(mapper); } @@ -51,7 +52,7 @@ public Future> call(AllProcess final BidRequest bidRequest = context.auctionContext().getBidRequest(); - final List corrections = correctionsProvider.corrections(config, bidRequest); + final List corrections = responseCorrectionProvider.corrections(config, bidRequest); if (corrections.isEmpty()) { return noAction(); } diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java index 5ea3b583acd..29e32743201 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionModule.java @@ -1,7 +1,7 @@ package org.prebid.server.hooks.modules.pb.response.correction.v1; import com.fasterxml.jackson.databind.ObjectMapper; -import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.Module; @@ -15,9 +15,9 @@ public class ResponseCorrectionModule implements Module { private final Collection> hooks; - public ResponseCorrectionModule(CorrectionsProvider correctionsProvider, ObjectMapper mapper) { + public ResponseCorrectionModule(ResponseCorrectionProvider responseCorrectionProvider, ObjectMapper mapper) { this.hooks = Collections.singleton( - new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, mapper)); + new ResponseCorrectionAllProcessedBidResponsesHook(responseCorrectionProvider, mapper)); } @Override diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java similarity index 90% rename from extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java rename to extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java index c5e7ac2d3f0..6b8fc33ba95 100644 --- a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/CorrectionsProviderTest.java +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/ResponseCorrectionProviderTest.java @@ -17,16 +17,16 @@ import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) -public class CorrectionsProviderTest { +public class ResponseCorrectionProviderTest { @Mock private CorrectionProducer correctionProducer; - private CorrectionsProvider target; + private ResponseCorrectionProvider target; @BeforeEach public void setUp() { - target = new CorrectionsProvider(singletonList(correctionProducer)); + target = new ResponseCorrectionProvider(singletonList(correctionProducer)); } @Test diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java index 0171f17cc04..0e525b06f24 100644 --- a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHookTest.java @@ -9,7 +9,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.hooks.modules.pb.response.correction.core.CorrectionsProvider; +import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; import org.prebid.server.hooks.v1.InvocationAction; @@ -34,7 +34,7 @@ public class ResponseCorrectionAllProcessedBidResponsesHookTest { private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); @Mock - private CorrectionsProvider correctionsProvider; + private ResponseCorrectionProvider responseCorrectionProvider; private ResponseCorrectionAllProcessedBidResponsesHook target; @@ -50,7 +50,7 @@ public void setUp() { given(invocationContext.auctionContext()) .willReturn(AuctionContext.builder().bidRequest(BidRequest.builder().build()).build()); - target = new ResponseCorrectionAllProcessedBidResponsesHook(correctionsProvider, MAPPER); + target = new ResponseCorrectionAllProcessedBidResponsesHook(responseCorrectionProvider, MAPPER); } @Test @@ -87,7 +87,7 @@ public void callShouldReturnNoActionOnDisabledConfig() { @Test public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { // given - given(correctionsProvider.corrections(any(), any())).willReturn(emptyList()); + given(responseCorrectionProvider.corrections(any(), any())).willReturn(emptyList()); // when final Future> result = target.call(payload, invocationContext); @@ -103,7 +103,7 @@ public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { public void callShouldReturnUpdate() { // given final Correction correction = mock(Correction.class); - given(correctionsProvider.corrections(any(), any())).willReturn(singletonList(correction)); + given(responseCorrectionProvider.corrections(any(), any())).willReturn(singletonList(correction)); // when final Future> result = target.call(payload, invocationContext); From 2f4667bac8f62a3f1907dadf3df09303b1195251 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:42:02 +0200 Subject: [PATCH 066/170] Ortb-Blocking: Aliases bug fix (#3447) --- .../v1/Ortb2BlockingBidderRequestHook.java | 3 +- .../Ortb2BlockingBidderRequestHookTest.java | 20 +- .../v1/model/BidderInvocationContextImpl.java | 25 + .../config/Ortb2BlockingActionOverride.groovy | 62 + .../config/Ortb2BlockingAttribute.groovy | 14 +- .../Ortb2BlockingAttributeConfig.groovy | 64 + .../config/Ortb2BlockingConditions.groovy | 16 + .../model/config/Ortb2BlockingConfig.groovy | 2 +- ...es.groovy => Ortb2BlockingOverride.groovy} | 6 +- .../model/response/auction/ExtModule.groovy | 2 + .../model/response/auction/MediaType.groovy | 3 + .../model/response/auction/ModuleError.groovy | 12 + .../response/auction/ModuleWarning.groovy | 12 + .../ortb2blocking/Ortb2BlockingSpec.groovy | 1241 ++++++++++++++++- 14 files changed, 1456 insertions(+), 26 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy rename src/test/groovy/org/prebid/server/functional/model/config/{Ortb2BlockingAttributes.groovy => Ortb2BlockingOverride.groovy} (64%) create mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java index 0bd01505596..ff9e6ab6c3c 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java @@ -42,7 +42,8 @@ public Future> call(BidderRequestPayload final BidRequest bidRequest = bidderRequestPayload.bidRequest(); final ModuleContext moduleContext = moduleContext(invocationContext) - .with(bidder, bidderSupportedOrtbVersion(bidder, aliases(bidRequest))); + .with(bidder, bidderSupportedOrtbVersion( + bidder, aliases(invocationContext.auctionContext().getBidRequest()))); final ExecutionResult blockedAttributesResult = BlockedAttributesResolver .create( diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index cb4e793f4fc..fe1ea6e9614 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -36,11 +36,14 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.spring.config.bidder.model.Ortb; +import java.util.Map; + import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @ExtendWith(MockitoExtension.class) @@ -50,10 +53,10 @@ public class Ortb2BlockingBidderRequestHookTest { .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private BidderCatalog bidderCatalog; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private BidRejectionTracker bidRejectionTracker; private Ortb2BlockingBidderRequestHook hook; @@ -68,10 +71,21 @@ public void setUp() { @Test public void shouldReturnResultWithNoActionWhenNoBlockingAttributes() { + // given + given(bidderCatalog.bidderInfoByName(anyString())) + .willReturn(bidderInfo(OrtbVersion.ORTB_2_6)); + given(bidderCatalog.bidderInfoByName(eq("bidder1Base"))) + .willReturn(bidderInfo(OrtbVersion.ORTB_2_5)); + // when final Future> result = hook.call( BidderRequestPayloadImpl.of(emptyRequest()), - BidderInvocationContextImpl.of("bidder1", bidRejectionTracker, null, true)); + BidderInvocationContextImpl.of( + "bidder1", + Map.of("bidder1", "bidder1Base"), + bidRejectionTracker, + null, + true)); // then assertThat(result.succeeded()).isTrue(); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java index d11d1912598..8b68c9279df 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; import lombok.Builder; import lombok.Value; import lombok.experimental.Accessors; @@ -9,6 +10,8 @@ import org.prebid.server.execution.Timeout; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.model.Endpoint; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import java.util.Map; @@ -39,6 +42,28 @@ public static BidderInvocationContext of(String bidder, return BidderInvocationContextImpl.builder() .bidder(bidder) .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) + .build()) + .accountConfig(accountConfig) + .debugEnabled(debugEnabled) + .build(); + } + + public static BidderInvocationContext of(String bidder, + Map aliases, + BidRejectionTracker bidRejectionTracker, + ObjectNode accountConfig, + boolean debugEnabled) { + + return BidderInvocationContextImpl.builder() + .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(aliases) + .build())) + .build()) .bidRejectionTrackers(Map.of(bidder, bidRejectionTracker)) .build()) .accountConfig(accountConfig) diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy new file mode 100644 index 00000000000..6e09273bef7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy @@ -0,0 +1,62 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingActionOverride { + + List enforceBlocks + List blockedAdomain + List blockedApp + List blockedBannerAttr + List blockedAdvCat + List blockedBannerType + + List blockUnknownAdomain + List blockUnknownAdvCat + + List allowedAdomainForDeals + List allowedAppForDeals + List allowedBannerAttrForDeals + List allowedAdvCatForDeals + + static Ortb2BlockingActionOverride getDefaultOverride(Ortb2BlockingAttribute attribute, + List blocked, + List allowedForDeals = null) { + + new Ortb2BlockingActionOverride().tap { + switch (attribute) { + case BADV: + blockedAdomain = blocked + allowedAdomainForDeals = allowedForDeals + break + case BAPP: + blockedApp = blocked + allowedAppForDeals = allowedForDeals + break + case BATTR: + blockedBannerAttr = blocked + allowedBannerAttrForDeals = allowedForDeals + break + case BCAT: + blockedAdvCat = blocked + allowedAdvCatForDeals = allowedForDeals + break + case BTYPE: + blockedBannerType = blocked + break + default: + throw new IllegalArgumentException("Unknown attribute type: $attribute") + } + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy index 827559c17cd..e1688e2d2b3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy @@ -1,13 +1,15 @@ package org.prebid.server.functional.model.config -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming +import com.fasterxml.jackson.annotation.JsonValue import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) -class Ortb2BlockingAttribute { +enum Ortb2BlockingAttribute { - Boolean enforceBlocks - List blockedAdomain + BADV, BAPP, BATTR, BCAT, BTYPE + + @JsonValue + String getValue() { + name().toLowerCase() + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy new file mode 100644 index 00000000000..5c405269466 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy @@ -0,0 +1,64 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingAttributeConfig { + + Boolean enforceBlocks + + Object blockedAdomain + Object blockedApp + Object blockedBannerAttr + Object blockedAdvCat + Object blockedBannerType + + Object blockUnknownAdomain + Object blockUnknownAdvCat + + Object allowedAdomainForDeals + Object allowedAppForDeals + Object allowedBannerAttrForDeals + Object allowedAdvCatForDeals + + Ortb2BlockingActionOverride actionOverrides + + static getDefaultConfig(Object ortb2Attributes, Ortb2BlockingAttribute attributeName, Object ortb2AttributesForDeals = null) { + new Ortb2BlockingAttributeConfig().tap { + enforceBlocks = false + switch (attributeName) { + case BADV: + blockedAdomain = ortb2Attributes + allowedAdomainForDeals = ortb2AttributesForDeals + break + case BAPP: + blockedApp = ortb2Attributes + allowedAppForDeals = ortb2AttributesForDeals + break + case BATTR: + blockedBannerAttr = ortb2Attributes + allowedBannerAttrForDeals = ortb2AttributesForDeals + break + case BCAT: + blockedAdvCat = ortb2Attributes + allowedAdvCatForDeals = ortb2AttributesForDeals + break + case BTYPE: + blockedBannerType = ortb2Attributes + break + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } + } + +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy new file mode 100644 index 00000000000..6e983374577 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConditions.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.response.auction.MediaType + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingConditions { + + List bidders + List mediaType + List dealIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy index 6b5b8f4adb0..1cef82cbe52 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingConfig.groovy @@ -5,5 +5,5 @@ import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) class Ortb2BlockingConfig { - Ortb2BlockingAttributes attributes + Map attributes } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy similarity index 64% rename from src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy rename to src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy index e5a3c13f5d9..987aa11e421 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributes.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingOverride.groovy @@ -5,7 +5,9 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) -class Ortb2BlockingAttributes { +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class Ortb2BlockingOverride { - Ortb2BlockingAttribute badv + Object override + Ortb2BlockingConditions conditions } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy index 32ce2fa727d..c0504a64cc4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ExtModule.groovy @@ -9,4 +9,6 @@ import groovy.transform.ToString class ExtModule { ModuleTrace trace + ModuleError errors + ModuleWarning warnings } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy index f3e376a30dd..5e46ef8425a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/MediaType.groovy @@ -8,12 +8,15 @@ enum MediaType { VIDEO, AUDIO, NATIVE, + WILDCARD, NULL @JsonValue String getValue() { if (name() == "NULL") { return null + } else if (name() == "WILDCARD") { + return "*" } name().toLowerCase() } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy new file mode 100644 index 00000000000..138b5e40507 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleError.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class ModuleError { + + Map> ortb2Blocking +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy new file mode 100644 index 00000000000..5c6d4ebed44 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleWarning.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class ModuleWarning { + + Map> ortb2Blocking +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy index d5eec884e9f..5b9d96e6bf3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -1,32 +1,1182 @@ package org.prebid.server.functional.tests.module.ortb2blocking +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingActionOverride +import org.prebid.server.functional.model.config.Ortb2BlockingAttributeConfig import org.prebid.server.functional.model.config.Ortb2BlockingAttribute -import org.prebid.server.functional.model.config.Ortb2BlockingAttributes +import org.prebid.server.functional.model.config.Ortb2BlockingConditions import org.prebid.server.functional.model.config.Ortb2BlockingConfig +import org.prebid.server.functional.model.config.Ortb2BlockingOverride import org.prebid.server.functional.model.config.PbsModulesConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED -import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class Ortb2BlockingSpec extends ModuleBaseSpec { - private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings) + private static final Map OPENX_CONFIG = ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final String WILDCARD = '*' + + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + OPENX_CONFIG) + + def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS should be able to send original array ortb2 attribute to bidder alias"() { + given: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.aliases = [(ALIAS.value): GENERIC] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.alias = new Generic() + } + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [ortb2Attributes]*.toString() + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, ortb2Attributes, attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.prebid.modules.errors.ortb2Blocking["ortb2-blocking-bidder-request"].first + .contains("field in account configuration is not an array") + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request shouldn't contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !getOrtb2Attributes(bidderRequest, attributeName) + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid for the called bidder" + assert response.ext.prebid.modules.errors.ortb2Blocking["ortb2-blocking-bidder-request"].first + .contains("field in account configuration has unexpected type") + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request shouldn't contain proper ortb2 attributes from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !getOrtb2Attributes(bidderRequest, attributeName) + + where: + ortb2Attributes | attributeName + PBSUtils.randomNumber | BADV + PBSUtils.randomNumber | BAPP + PBSUtils.randomNumber | BCAT + PBSUtils.randomString | BATTR + PBSUtils.randomString | BTYPE + } + + def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + } + + def "PBS should send only not matched ortb2 attribute to bidder when blocking is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName), + getBidWithOrtb2Attribute(bidRequest.imp.first, allowedOrtb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + allowedOrtb2Attributes | disallowedOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = false + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain proper seatbid" + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + } + + def "PBS should discard unknown adomain bids when enforcement is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def allowedOrtb2Attributes = PBSUtils.randomString + def bidPrice = PBSUtils.randomPrice + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + price = bidPrice + 1 // to guarantee higher priority by default settings + } + def bidWithAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = [allowedOrtb2Attributes] + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain, bidWithAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, BADV) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should not discard unknown adomain bids when enforcement is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2BlockingAttributeConfig << [new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: false), + new Ortb2BlockingAttributeConfig(enforceBlocks: false, blockUnknownAdomain: true), + new Ortb2BlockingAttributeConfig(enforceBlocks: true)] + } + + def "PBS should discard unknown adv cat bids when enforcement is enabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def allowedOrtb2Attributes = PBSUtils.randomString + def bidPrice = PBSUtils.randomPrice + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + price = bidPrice + 1 // to guarantee higher priority by default settings + } + def bidWithAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = [allowedOrtb2Attributes] + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain, bidWithAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, BCAT) == [allowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should not discard unknown adv cat bids when enforcement is disabled"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [bidWithOutAdomain] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2BlockingAttributeConfig << [new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: false), + new Ortb2BlockingAttributeConfig(enforceBlocks: false, blockUnknownAdvCat: true), + new Ortb2BlockingAttributeConfig(enforceBlocks: true)] + } + + def "PBS should not discard bids with deals when allowed ortb2 attribute for deals is matched"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def attributes = [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2Attributes]).tap { + enforceBlocks = true + }] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = PBSUtils.randomNumber }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + } + + def "PBS should discard bids with deals when allowed ortb2 attribute for deals is not matched"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def attributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([allowedOrtb2Attributes, dielsOrtb2Attributes], attributeName, [allowedOrtb2Attributes]).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): attributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, dielsOrtb2Attributes, attributeName) + .tap { dealid = PBSUtils.randomNumber }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid.bid.flatten().size() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + allowedOrtb2Attributes | dielsOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should be able to override enforcement by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC), + new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: OPENX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only openx seatbid" + assert response.seatbid.size() == 1 + assert response.seatbid.first.seat == OPENX + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + PBSUtils.randomNumber | BATTR + } + + def "PBS should be able to override enforcement by media type"() { + given: "Default bidRequest" + def bannerImp = Imp.getDefaultImpression(BANNER) + def videoImp = Imp.getDefaultImpression(VIDEO) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [bannerImp, videoImp] + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bannerImp, ortb2Attributes, attributeName)]), + new SeatBid(bid: [getBidWithOrtb2Attribute(videoImp, ortb2Attributes, attributeName)])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.impid == bannerImp.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + PBSUtils.randomString | BADV + PBSUtils.randomString | BAPP + PBSUtils.randomString | BCAT + } + + def "PBS should be able to override enforcement by media type for battr attribute"() { + given: "Default bidRequest" + def bannerImp = Imp.getDefaultImpression(BANNER) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [bannerImp] + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], BATTR).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BATTR): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bannerImp, ortb2Attribute, BATTR)])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.impid == bannerImp.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, BATTR) == [ortb2Attribute]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + @PendingFeature + def "PBS should be able to override enforcement by deal id"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + dealId | ortb2Attributes | attributeName + PBSUtils.randomNumber | PBSUtils.randomString | BADV + PBSUtils.randomNumber | PBSUtils.randomString | BAPP + PBSUtils.randomNumber | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + WILDCARD | PBSUtils.randomString | BADV + WILDCARD | PBSUtils.randomString | BAPP + WILDCARD | PBSUtils.randomString | BCAT + WILDCARD | PBSUtils.randomNumber | BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [GENERIC]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [overrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [ortb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [overrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | overrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should be able to override blocked ortb2 attribute by media type"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [overrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [ortb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [overrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | overrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should be able to override block unknown adomain by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdomain: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutAdomain], seat: GENERIC), + new SeatBid(bid: [bidWithOutAdomain], seat: OPENX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only openx seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.seat == OPENX + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adomain by media type"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdomain: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutAdomain = Bid.getDefaultBid(bidRequest.imp.first).tap { + adomain = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutAdomain])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adv-cat by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + + and: "Account in the DB with blocking configuration" + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdvCat: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutCat = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutCat], seat: GENERIC), + new SeatBid(bid: [bidWithOutCat], seat: OPENX)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only openx seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.seat == OPENX + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override block unknown adv-cat by media type"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { + actionOverrides = new Ortb2BlockingActionOverride(blockUnknownAdvCat: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidWithOutCat = Bid.getDefaultBid(bidRequest.imp.first).tap { + cat = null + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bidWithOutCat])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain banner seatbid" + assert response.seatbid.bid.flatten().size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + } + + def "PBS should be able to override allowed ortb2 attribute for deals by deal ids"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def dealId = PBSUtils.randomNumber + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [dealOverrideAttributes]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [ortb2BlockingOverride]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | dealOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should use first override when multiple match same condition"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: blockingCondition) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [firstOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response should contain proper warning" + assert response?.ext?.prebid?.modules?.warnings?.ortb2Blocking["ortb2-blocking-bidder-request"] == + ["More than one conditions matches request. Bidder: generic, request media types: [banner]"] + + where: + blockingCondition | ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by bidder"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [BidderName.WILDCARD])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [GENERIC])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [secondOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should prefer non wildcard override when multiple match same condition by media type"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [MediaType.WILDCARD])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [BANNER])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should override blocked ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == [secondOverrideAttributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + } + + def "PBS should merge allowed bundle for deals overrides together"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB with blocking configuration" + def dealId = PBSUtils.randomNumber + def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) + def ortb2BlockingOverride = new Ortb2BlockingOverride(override: [ortb2Attributes.last], conditions: blockingCondition) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig(ortb2Attributes, attributeName, [ortb2Attributes.first]).tap { + enforceBlocks = true + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [ortb2BlockingOverride]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName) + .tap { dealid = dealId }] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only seatbid with proper deal id" + assert response.seatbid.bid.flatten().size() == 1 + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == ortb2Attributes*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + ortb2Attributes | attributeName + [PBSUtils.randomString, PBSUtils.randomString] | BADV + [PBSUtils.randomString, PBSUtils.randomString] | BCAT + [PBSUtils.randomNumber, PBSUtils.randomNumber] | BATTR + } + + def "PBS should not be override from config when ortb2 attribute present in incoming request"() { + given: "Account in the DB with blocking configuration" + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request should contain original ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == getOrtb2Attributes(bidRequest, attributeName) + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + bidRequest | ortb2Attributes | attributeName + BidRequest.defaultBidRequest.tap { badv = [PBSUtils.randomString] } | PBSUtils.randomString | BADV + BidRequest.defaultBidRequest.tap { bapp = [PBSUtils.randomString] } | PBSUtils.randomString | BAPP + BidRequest.defaultBidRequest.tap { bcat = [PBSUtils.randomString] } | PBSUtils.randomString | BCAT + BidRequest.defaultBidRequest.tap { imp[0].banner.battr = [PBSUtils.randomNumber] } | PBSUtils.randomNumber | BATTR + BidRequest.defaultBidRequest.tap { imp[0].banner.btype = [PBSUtils.randomNumber] } | PBSUtils.randomNumber | BTYPE + } def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with rejected advertiser blocked status code"() { - given: "Default account with return bid status" + given: "Default bidRequest with returnAllBidStatus attribute" def bidRequest = BidRequest.defaultBidRequest.tap { it.ext.prebid.returnAllBidStatus = true } @@ -34,18 +1184,13 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { and: "Default bidder response with aDomain" def aDomain = PBSUtils.randomString def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - it.seatbid.first.bid.first.adomain = [aDomain] + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, aDomain, BADV)] } bidder.setResponse(bidRequest.id, bidResponse) and: "Account in the DB with blocking configuration" - def blockingAttributes = new Ortb2BlockingAttributes(badv: new Ortb2BlockingAttribute(enforceBlocks: true, blockedAdomain: [aDomain])) - def blockingConfig = new Ortb2BlockingConfig(attributes: blockingAttributes) - def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [BIDDER_REQUEST, RAW_BIDDER_RESPONSE]) - def richMediaFilterConfig = new PbsModulesConfig(ortb2Blocking: blockingConfig) - def accountHooksConfig = new AccountHooksConfiguration(executionPlan: executionPlan, modules: richMediaFilterConfig) - def accountConfig = new AccountConfig(hooks: accountHooksConfig) - def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + def attributes = [(BADV): new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockedAdomain: [aDomain])] + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, attributes) accountDao.save(account) when: "PBS processes the auction request" @@ -55,8 +1200,78 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == ErrorType.GENERIC.value assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_ADVERTISER_BLOCKED } + + private static Account getAccountWithOrtb2BlockingConfig(String accountId, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { + getAccountWithOrtb2BlockingConfig(accountId, [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig(ortb2Attributes, attributeName)]) + } + + private static Account getAccountWithOrtb2BlockingConfig(String accountId, Map attributes) { + def blockingConfig = new Ortb2BlockingConfig(attributes: attributes) + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB2_BLOCKING, [BIDDER_REQUEST, RAW_BIDDER_RESPONSE]) + def moduleConfig = new PbsModulesConfig(ortb2Blocking: blockingConfig) + def accountHooksConfig = new AccountHooksConfiguration(executionPlan: executionPlan, modules: moduleConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: accountId, config: accountConfig) + } + + private static Bid getBidWithOrtb2Attribute(Imp imp, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { + Bid.getDefaultBid(imp).tap { + switch (attributeName) { + case BADV: + adomain = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BAPP: + bundle = (ortb2Attributes instanceof List) ? ortb2Attributes.first : ortb2Attributes + break + case BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BCAT: + cat = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case BTYPE: + break + default: + throw new IllegalArgumentException("Unknown ortb2 attribute: $attributeName") + } + } + } + + private static List getOrtb2Attributes(BidRequest bidRequest, Ortb2BlockingAttribute attributeName) { + switch (attributeName) { + case BADV: + return bidRequest.badv + case BAPP: + return bidRequest.bapp + case BATTR: + return bidRequest.imp[0].banner.battr*.toString() + case BCAT: + return bidRequest.bcat + case BTYPE: + return bidRequest.imp[0].banner.btype*.toString() + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } + + private static List getOrtb2Attributes(Bid bid, Ortb2BlockingAttribute attributeName) { + switch (attributeName) { + case BADV: + return bid.adomain + case BAPP: + return [bid.bundle] + case BATTR: + return bid.attr*.toString() + case BCAT: + return bid.cat + case BTYPE: + return null + default: + throw new IllegalArgumentException("Unknown attribute type: $attributeName") + } + } } From d241304470f9c99da0a92378d979ab63ac08a1f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:47:57 +0200 Subject: [PATCH 067/170] Core: Bump protobuf-java (#3451) --- extra/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/pom.xml b/extra/pom.xml index 2049380670c..25987cb8260 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -49,8 +49,8 @@ 2.0.10 3.2.0 2.12.0 - 3.21.7 - 3.17.3 + 3.25.5 + ${protobuf.version} 1.0.7 2.26.24 From 4b2929a99fff1f71e074f3c05c77134e745f862d Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:58:19 +0200 Subject: [PATCH 068/170] TGM: Add limelightdigital Alias (#3455) --- .../bidder-config/limelightDigital.yaml | 2 + .../java/org/prebid/server/it/TgmTest.java | 35 ++++++++++++++++ .../tgm/test-auction-tgm-request.json | 24 +++++++++++ .../tgm/test-auction-tgm-response.json | 33 +++++++++++++++ .../it/openrtb2/tgm/test-tgm-bid-request.json | 40 +++++++++++++++++++ .../openrtb2/tgm/test-tgm-bid-response.json | 15 +++++++ .../server/it/test-application.properties | 2 + 7 files changed, 151 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/TgmTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-response.json diff --git a/src/main/resources/bidder-config/limelightDigital.yaml b/src/main/resources/bidder-config/limelightDigital.yaml index f13e008afd0..16d495c8d55 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -36,6 +36,8 @@ adapters: embimedia: enabled: false endpoint: http://ads-pbs.bidder-embi.media/openrtb/{{PublisherID}}?host={{Host}} + tgm: + enabled: false meta-info: maintainer-email: engineering@project-limelight.com app-media-types: diff --git a/src/test/java/org/prebid/server/it/TgmTest.java b/src/test/java/org/prebid/server/it/TgmTest.java new file mode 100644 index 00000000000..e94e25abe08 --- /dev/null +++ b/src/test/java/org/prebid/server/it/TgmTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class TgmTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheTgmBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/tgm-exchange/test.host/123456")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/tgm/test-tgm-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/tgm/test-tgm-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/tgm/test-auction-tgm-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/tgm/test-auction-tgm-response.json", response, + singletonList("tgm")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json new file mode 100644 index 00000000000..82490a56da6 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tgm": { + "host": "test.host", + "publisherId": "123456" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json new file mode 100644 index 00000000000..a312ae577d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json @@ -0,0 +1,33 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "tgm", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "tgm": "{{ tgm.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-request.json new file mode 100644 index 00000000000..8e58e53ba4b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-request.json @@ -0,0 +1,40 @@ +{ + "id": "request_id-imp_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-response.json new file mode 100644 index 00000000000..04d26e04318 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-tgm-bid-response.json @@ -0,0 +1,15 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} 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 eebc53e085f..853cf7c4652 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -277,6 +277,8 @@ adapters.limelightDigital.aliases.embimedia.enabled=true adapters.limelightDigital.aliases.embimedia.endpoint=http://localhost:8090/embimedia-exchange/{{Host}}/{{PublisherID}} adapters.limelightDigital.aliases.filmzie.enabled=true adapters.limelightDigital.aliases.filmzie.endpoint=http://localhost:8090/filmzie-exchange/{{Host}}/{{PublisherID}} +adapters.limelightDigital.aliases.tgm.enabled=true +adapters.limelightDigital.aliases.tgm.endpoint=http://localhost:8090/tgm-exchange/{{Host}}/{{PublisherID}} adapters.lmkiviads.enabled=true adapters.lmkiviads.endpoint=http://localhost:8090/lm-kiviads-exchange/{{SourceId}}/{{Host}} adapters.lockerdome.enabled=true From 8a43d314e252722dbb93d27c68a4eb95c36035ab Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:46:58 +0200 Subject: [PATCH 069/170] Agma: Analytics Adapter (#3419) --- .../reporter/agma/AgmaAnalyticsReporter.java | 264 ++++++++++ .../analytics/reporter/agma/EventBuffer.java | 59 +++ .../model/AgmaAccountAnalyticsProperties.java | 15 + .../agma/model/AgmaAnalyticsProperties.java | 26 + .../reporter/agma/model/AgmaEvent.java | 38 ++ .../spring/config/AnalyticsConfiguration.java | 106 ++++ src/main/resources/application.yaml | 12 + .../agma/AgmaAnalyticsReporterTest.java | 479 ++++++++++++++++++ .../reporter/agma/EventBufferTest.java | 48 ++ 9 files changed, 1047 insertions(+) create mode 100644 src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java create mode 100644 src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java create mode 100644 src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java create mode 100644 src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java create mode 100644 src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java create mode 100644 src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java create mode 100644 src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java new file mode 100644 index 00000000000..dc9143966f3 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -0,0 +1,264 @@ +package org.prebid.server.analytics.reporter.agma; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iabtcf.decoder.TCString; +import com.iabtcf.utils.IntIterable; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; +import org.prebid.server.analytics.reporter.agma.model.AgmaEvent; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.gdpr.vendorlist.proto.PurposeCode; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.Initializable; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.zip.GZIPOutputStream; + +public class AgmaAnalyticsReporter implements AnalyticsReporter, Initializable { + + private static final Logger logger = LoggerFactory.getLogger(AgmaAnalyticsReporter.class); + + private final String url; + private final boolean compressToGzip; + private final long httpTimeoutMs; + + private final EventBuffer buffer; + + private final Map accounts; + + private final Vertx vertx; + private final JacksonMapper jacksonMapper; + private final HttpClient httpClient; + private final Clock clock; + private final MultiMap headers; + + public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper jacksonMapper, + Clock clock, + HttpClient httpClient, + Vertx vertx) { + + this.accounts = agmaAnalyticsProperties.getAccounts(); + + this.url = HttpUtil.validateUrl(agmaAnalyticsProperties.getUrl()); + this.httpTimeoutMs = agmaAnalyticsProperties.getHttpTimeoutMs(); + this.compressToGzip = agmaAnalyticsProperties.isGzip(); + + this.buffer = new EventBuffer<>( + agmaAnalyticsProperties.getMaxEventsCount(), + agmaAnalyticsProperties.getBufferSize()); + + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.vertx = Objects.requireNonNull(vertx); + this.clock = Objects.requireNonNull(clock); + this.headers = makeHeaders(Objects.requireNonNull(prebidVersionProvider)); + } + + @Override + public void initialize(Promise initializePromise) { + vertx.setPeriodic(1000L, ignored -> sendEvents(buffer.pollAll())); + initializePromise.complete(); + } + + @Override + public Future processEvent(T event) { + final Pair contextAndType = switch (event) { + case AuctionEvent auctionEvent -> Pair.of(auctionEvent.getAuctionContext(), "auction"); + case AmpEvent ampEvent -> Pair.of(ampEvent.getAuctionContext(), "amp"); + case VideoEvent videoEvent -> Pair.of(videoEvent.getAuctionContext(), "video"); + case null, default -> null; + }; + + if (contextAndType == null) { + return Future.succeededFuture(); + } + + final AuctionContext auctionContext = contextAndType.getLeft(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final TimeoutContext timeoutContext = auctionContext.getTimeoutContext(); + final PrivacyContext privacyContext = auctionContext.getPrivacyContext(); + + if (!allowedToSendEvent(bidRequest, privacyContext)) { + return Future.succeededFuture(); + } + + final String accountCode = Optional.ofNullable(bidRequest) + .map(AgmaAnalyticsReporter::getPublisherId) + .map(accounts::get) + .orElse(null); + + if (accountCode == null) { + return Future.succeededFuture(); + } + + final AgmaEvent agmaEvent = AgmaEvent.builder() + .eventType(contextAndType.getRight()) + .accountCode(accountCode) + .requestId(bidRequest.getId()) + .app(bidRequest.getApp()) + .site(bidRequest.getSite()) + .device(bidRequest.getDevice()) + .user(bidRequest.getUser()) + .startTime(ZonedDateTime.ofInstant( + Instant.ofEpochMilli(timeoutContext.getStartTime()), clock.getZone())) + .build(); + + final String eventString = jacksonMapper.encodeToString(agmaEvent); + buffer.put(eventString, eventString.length()); + final List toFlush = buffer.pollToFlush(); + if (!toFlush.isEmpty()) { + sendEvents(toFlush); + } + + return Future.succeededFuture(); + } + + private boolean allowedToSendEvent(BidRequest bidRequest, PrivacyContext privacyContext) { + final TCString consent = Optional.ofNullable(privacyContext) + .map(PrivacyContext::getTcfContext) + .map(TcfContext::getConsent) + .or(() -> Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ExtUser::getConsent) + .map(AgmaAnalyticsReporter::decodeConsent)) + .orElse(null); + + if (consent == null) { + return false; + } + + final IntIterable purposesConsent = consent.getPurposesConsent(); + final IntIterable vendorConsent = consent.getVendorConsent(); + + final boolean isPurposeAllowed = purposesConsent.contains(PurposeCode.NINE.code()); + final boolean isVendorAllowed = vendorConsent.contains(vendorId()); + return isPurposeAllowed && isVendorAllowed; + } + + private static TCString decodeConsent(String consent) { + try { + return TCString.decode(consent); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static String getPublisherId(BidRequest bidRequest) { + final Site site = bidRequest.getSite(); + final App app = bidRequest.getApp(); + + final String publisherId = Optional.ofNullable(site).map(Site::getPublisher).map(Publisher::getId) + .or(() -> Optional.ofNullable(app).map(App::getPublisher).map(Publisher::getId)) + .orElse(null); + final String appSiteId = Optional.ofNullable(site).map(Site::getId) + .or(() -> Optional.ofNullable(app).map(App::getId)) + .or(() -> Optional.ofNullable(app).map(App::getBundle)) + .orElse(null); + + if (publisherId == null && appSiteId == null) { + return null; + } + + return publisherId; + } + + private void sendEvents(List events) { + final String payload = preparePayload(events); + final Future responseFuture = compressToGzip + ? httpClient.request(HttpMethod.POST, url, headers, gzip(payload), httpTimeoutMs) + : httpClient.request(HttpMethod.POST, url, headers, payload, httpTimeoutMs); + + responseFuture.onComplete(this::handleReportResponse); + } + + private static String preparePayload(List events) { + return "[" + String.join(",", events) + "]"; + } + + private static byte[] gzip(String value) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { + + gzip.write(value.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + + return obj.toByteArray(); + } catch (IOException e) { + throw new PreBidException("[agmaAnalytics] failed to compress, skip the events : " + e.getMessage()); + } + } + + private void handleReportResponse(AsyncResult result) { + if (result.failed()) { + logger.error("[agmaAnalytics] Failed to send events to endpoint {} with a reason: {}", + url, result.cause().getMessage()); + } else { + final HttpClientResponse httpClientResponse = result.result(); + final int statusCode = httpClientResponse.getStatusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + logger.error("[agmaAnalytics] Wrong code received {} instead of 200", statusCode); + } + } + } + + private MultiMap makeHeaders(PrebidVersionProvider versionProvider) { + final MultiMap headers = MultiMap.caseInsensitiveMultiMap() + .add(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON) + .add(HttpUtil.X_PREBID_HEADER, versionProvider.getNameVersionRecord()); + + if (compressToGzip) { + headers.add(HttpHeaders.CONTENT_ENCODING, HttpHeaderValues.GZIP); + } + + return headers; + } + + @Override + public int vendorId() { + return 1122; + } + + @Override + public String name() { + return "agmaAnalytics"; + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java b/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java new file mode 100644 index 00000000000..d291fa0ba1c --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/EventBuffer.java @@ -0,0 +1,59 @@ +package org.prebid.server.analytics.reporter.agma; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class EventBuffer { + + private final Lock lock = new ReentrantLock(true); + + private List events = new ArrayList<>(); + + private long byteSize = 0; + + private final long maxEvents; + + private final long maxBytes; + + public EventBuffer(long maxEvents, long maxBytes) { + this.maxEvents = maxEvents; + this.maxBytes = maxBytes; + } + + public void put(T event, long eventSize) { + lock.lock(); + events.addLast(event); + byteSize += eventSize; + lock.unlock(); + } + + public List pollToFlush() { + List toFlush = Collections.emptyList(); + + lock.lock(); + if (events.size() >= maxEvents || byteSize >= maxBytes) { + toFlush = events; + reset(); + } + lock.unlock(); + + return toFlush; + } + + public List pollAll() { + lock.lock(); + final List polled = events; + reset(); + lock.unlock(); + + return polled; + } + + private void reset() { + byteSize = 0; + events = new ArrayList<>(); + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java new file mode 100644 index 00000000000..1d5a994dbe9 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAccountAnalyticsProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class AgmaAccountAnalyticsProperties { + + String code; + + String publisherId; + + String siteAppId; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java new file mode 100644 index 00000000000..c1d5ed0c4ed --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaAnalyticsProperties.java @@ -0,0 +1,26 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class AgmaAnalyticsProperties { + + String url; + + boolean gzip; + + Integer bufferSize; + + Integer maxEventsCount; + + Long bufferTimeoutMs; + + Long httpTimeoutMs; + + Map accounts; + +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java new file mode 100644 index 00000000000..51e385744bf --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/model/AgmaEvent.java @@ -0,0 +1,38 @@ +package org.prebid.server.analytics.reporter.agma.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import lombok.Builder; +import lombok.Value; + +import java.time.ZonedDateTime; + +@Value +@Builder +public class AgmaEvent { + + @JsonProperty("type") + String eventType; + + @JsonProperty("id") + String requestId; + + @JsonProperty("code") + String accountCode; + + Site site; + + App app; + + Device device; + + User user; + + //format 2023-02-01T00:00:00Z + @JsonProperty("created_at") + ZonedDateTime startTime; + +} diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java index 688cb0c5efb..ea8b68d0ebc 100644 --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java @@ -4,8 +4,11 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.analytics.reporter.agma.AgmaAnalyticsReporter; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; import org.prebid.server.analytics.reporter.greenbids.GreenbidsAnalyticsReporter; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; import org.prebid.server.analytics.reporter.log.LogAnalyticsReporter; @@ -25,9 +28,12 @@ import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Configuration public class AnalyticsConfiguration { @@ -56,6 +62,106 @@ LogAnalyticsReporter logAnalyticsReporter(JacksonMapper mapper) { return new LogAnalyticsReporter(mapper); } + @Configuration + @ConditionalOnProperty(prefix = "analytics.agma", name = "enabled", havingValue = "true") + public static class AgmaAnalyticsConfiguration { + + @Bean + AgmaAnalyticsReporter agmaAnalyticsReporter(AgmaAnalyticsConfigurationProperties properties, + JacksonMapper jacksonMapper, + HttpClient httpClient, + Clock clock, + PrebidVersionProvider prebidVersionProvider, + Vertx vertx) { + + return new AgmaAnalyticsReporter( + properties.toComponentProperties(), + prebidVersionProvider, + jacksonMapper, + clock, + httpClient, + vertx); + } + + @Bean + @ConfigurationProperties(prefix = "analytics.agma") + AgmaAnalyticsConfigurationProperties agmaAnalyticsConfigurationProperties() { + return new AgmaAnalyticsConfigurationProperties(); + } + + @Validated + @NoArgsConstructor + @Data + private static class AgmaAnalyticsConfigurationProperties { + + @NotNull + private AgmaAnalyticsHttpEndpointProperties endpoint; + + @NotNull + private AgmaAnalyticsBufferProperties buffers; + + @NotEmpty(message = "Please configure at least one account for Agma Analytics") + private List accounts; + + public AgmaAnalyticsProperties toComponentProperties() { + final Map accountsByPublisherId = accounts.stream() + .collect(Collectors.toMap( + AgmaAnalyticsAccountProperties::getPublisherId, + AgmaAnalyticsAccountProperties::getCode)); + + return AgmaAnalyticsProperties.builder() + .url(endpoint.getUrl()) + .gzip(BooleanUtils.isTrue(endpoint.getGzip())) + .bufferSize(buffers.getSizeBytes()) + .maxEventsCount(buffers.getCount()) + .bufferTimeoutMs(buffers.getTimeoutMs()) + .httpTimeoutMs(endpoint.getTimeoutMs()) + .accounts(accountsByPublisherId) + .build(); + } + + @Validated + @NoArgsConstructor + @Data + private static class AgmaAnalyticsHttpEndpointProperties { + + @NotNull + private String url; + + @NotNull + private Long timeoutMs; + + private Boolean gzip; + } + + @NoArgsConstructor + @Data + private static class AgmaAnalyticsBufferProperties { + + @NotNull + private Integer sizeBytes; + + @NotNull + private Integer count; + + @NotNull + private Long timeoutMs; + } + + @NoArgsConstructor + @Data + private static class AgmaAnalyticsAccountProperties { + + private String code; + + @NotNull + private String publisherId; + + private String siteAppId; + } + } + } + @Configuration @ConditionalOnProperty(prefix = "analytics.greenbids", name = "enabled", havingValue = "true") public static class GreenbidsAnalyticsConfiguration { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 357a7a5220d..c9e1eb589e3 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -298,5 +298,17 @@ analytics: analytics-server: http://localhost:8090 exploratory-sampling-split: 0.9 timeout-ms: 10000 + agma: + enabled: false + accounts: + - code: code + publisher-id: pub + buffers: + size-bytes: 100000 + timeout-ms: 5000 + count: 4 + endpoint: + url: http:/url.com + timeout-ms: 5000 price-floors: enabled: false diff --git a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java new file mode 100644 index 00000000000..2427942605e --- /dev/null +++ b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java @@ -0,0 +1,479 @@ +package org.prebid.server.analytics.reporter.agma; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iabtcf.decoder.TCString; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.analytics.model.AmpEvent; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.analytics.model.VideoEvent; +import org.prebid.server.analytics.reporter.agma.model.AgmaAnalyticsProperties; +import org.prebid.server.analytics.reporter.agma.model.AgmaEvent; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.version.PrebidVersionProvider; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import static io.vertx.core.http.HttpMethod.POST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class AgmaAnalyticsReporterTest extends VertxTest { + + private static final String VALID_CONSENT = + "CQEXy8AQEXy8APoABABGBFEAAACAAAAAAAAAIxQAQIxAAAAA.QIxQAQIxAAAA.IAAA"; + private static final String CONSENT_WITHOUT_PURPOSE_9 = + "CQEXy8AQEXy8APoABABGBFEAAACAAAAAAAAAIxQAQIxAAAAA.QIxQAQIxAAAA.IAAA"; + private static final String CONSENT_WITHOUT_VENDOR = + "CQEXy8AQEXy8APoABABGBFEAAACAAAAAAAAAIwwAQIwgAAAA.QJJQAQJJAAAA.IAAA"; + + private static final TCString PARSED_VALID_CONSENT = TCString.decode(VALID_CONSENT); + + @Mock(strictness = Mock.Strictness.LENIENT) + private Vertx vertx; + + @Mock(strictness = Mock.Strictness.LENIENT) + private HttpClient httpClient; + + @Mock + private PrebidVersionProvider versionProvider; + + @Captor + private ArgumentCaptor headersCaptor; + + private Clock clock; + + private AgmaAnalyticsReporter target; + + @BeforeEach + public void setUp() { + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of( + "publisherId", "accountCode", + "unknown_publisherId", "anotherCode")) + .build(); + + clock = Clock.fixed(Instant.parse("2024-09-03T10:00:00Z"), ZoneId.of("UTC+05:00")); + + given(versionProvider.getNameVersionRecord()).willReturn("pbs_version"); + given(vertx.setTimer(anyLong(), any())).willReturn(1L, 2L); + given(httpClient.request(eq(POST), anyString(), any(), anyString(), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), ""))); + given(httpClient.request(eq(POST), anyString(), any(), any(byte[].class), anyLong())).willReturn( + Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), ""))); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + } + + @Test + public void processEventShouldSendEventWhenEventIsAuctionEvent() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendEventWhenEventIsVideoEvent() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final VideoEvent videoEvent = VideoEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(videoEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("video") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendEventWhenEventIsAmpEvent() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AmpEvent ampEvent = AmpEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(ampEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("amp") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenEventIsNotAuctionAmpOrVideo() { + // given + final NotificationEvent notificationEvent = NotificationEvent.builder().build(); + + // when + final Future result = target.processEvent(notificationEvent); + + // then + assertThat(result.succeeded()).isTrue(); + verifyNoInteractions(httpClient); + } + + @Test + public void processEventShouldSendEventWhenConsentIsValidButWasParsedFromUserExt() { + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + final App givenApp = App.builder().build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().ext(ExtUser.builder().consent(VALID_CONSENT).build()).build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + any(), + eq(expectedEventPayload), + eq(1000L)); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenVendorIsNotAllowed() { + // given + final User givenUser = User.builder().ext(ExtUser.builder().consent(CONSENT_WITHOUT_VENDOR).build()).build(); + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().user(givenUser).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenPurposeIsNotAllowed() { + // given + final User givenUser = User.builder().ext(ExtUser.builder().consent(CONSENT_WITHOUT_PURPOSE_9).build()).build(); + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().user(givenUser).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldNotSendAnythingWhenAccountsDoesNotHaveConfiguredPublisher() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("unknown_publisherId", "anotherCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + // given + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + + final AmpEvent ampEvent = AmpEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .bidRequest(BidRequest.builder().site(givenSite).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(ampEvent); + + // then + verifyNoInteractions(httpClient); + assertThat(result.succeeded()).isTrue(); + } + + @Test + public void processEventShouldSendEncodingGzipHeaderAndCompressedPayload() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(true) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("publisherId", "accountCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + final Site givenSite = Site.builder().publisher(Publisher.builder().id("publisherId").build()).build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder().site(givenSite).build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .site(givenSite) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + headersCaptor.capture(), + aryEq(gzip(expectedEventPayload)), + eq(1000L)); + + assertThat(headersCaptor.getValue()) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsOnly( + tuple("content-type", "application/json"), + tuple("content-encoding", "gzip"), + tuple("x-prebid", "pbs_version")); + + assertThat(result.succeeded()).isTrue(); + } + + private static byte[] gzip(String value) { + try (ByteArrayOutputStream obj = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(obj)) { + + gzip.write(value.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + + return obj.toByteArray(); + } catch (IOException e) { + return new byte[]{}; + } + } +} diff --git a/src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java new file mode 100644 index 00000000000..c58222f703b --- /dev/null +++ b/src/test/java/org/prebid/server/analytics/reporter/agma/EventBufferTest.java @@ -0,0 +1,48 @@ +package org.prebid.server.analytics.reporter.agma; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EventBufferTest { + + @Test + public void pollToFlushShouldReturnEventsToFlushWhenMaxEventsExceeded() { + // given + final EventBuffer target = new EventBuffer<>(1, 999); + target.put("test", 4); + + // when and then + assertThat(target.pollToFlush()).containsExactly("test"); + } + + @Test + public void pollToFlushShouldReturnEventsToFlushWhenMaxBytesExceeded() { + // given + final EventBuffer target = new EventBuffer<>(999, 1); + target.put("test", 4); + + // when and then + assertThat(target.pollToFlush()).containsExactly("test"); + } + + @Test + public void pollToFlushShouldNotReturnAnyEventsWhenLimitsAreNotExceeded() { + // given + final EventBuffer target = new EventBuffer<>(999, 999); + target.put("test", 4); + + // when and then + assertThat(target.pollToFlush()).isEmpty(); + } + + @Test + public void pollAllShouldReturnAllEvents() { + // given + final EventBuffer target = new EventBuffer<>(999, 999); + target.put("test", 4); + + // when and then + assertThat(target.pollAll()).containsExactly("test"); + } +} From 22fb33146efedb151cd8cca859f1d40b6390e40a Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Mon, 23 Sep 2024 16:56:11 +0300 Subject: [PATCH 070/170] Agma: Leftovers (#3458) --- .../server/analytics/reporter/agma/AgmaAnalyticsReporter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java index dc9143966f3..93665840a21 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -57,6 +57,7 @@ public class AgmaAnalyticsReporter implements AnalyticsReporter, Initializable { private final String url; private final boolean compressToGzip; + private final long bufferTimeoutMs; private final long httpTimeoutMs; private final EventBuffer buffer; @@ -79,6 +80,7 @@ public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, this.accounts = agmaAnalyticsProperties.getAccounts(); this.url = HttpUtil.validateUrl(agmaAnalyticsProperties.getUrl()); + this.bufferTimeoutMs = agmaAnalyticsProperties.getBufferTimeoutMs(); this.httpTimeoutMs = agmaAnalyticsProperties.getHttpTimeoutMs(); this.compressToGzip = agmaAnalyticsProperties.isGzip(); @@ -95,7 +97,7 @@ public AgmaAnalyticsReporter(AgmaAnalyticsProperties agmaAnalyticsProperties, @Override public void initialize(Promise initializePromise) { - vertx.setPeriodic(1000L, ignored -> sendEvents(buffer.pollAll())); + vertx.setPeriodic(bufferTimeoutMs, ignored -> sendEvents(buffer.pollAll())); initializePromise.complete(); } From 01fa5b797a9095c5d2a2915e63245bf712c9352c Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Tue, 24 Sep 2024 15:24:48 +0200 Subject: [PATCH 071/170] Core: Configure analytics adapters per account (#3443) --- docs/application-settings.md | 1 + docs/config-app.md | 1 + .../server/analytics/model/AmpEvent.java | 2 +- .../reporter/AnalyticsReporterDelegator.java | 146 ++++++++++++- .../spring/config/AnalyticsConfiguration.java | 9 +- src/main/resources/application.yaml | 2 + .../config/AccountAnalyticsConfig.groovy | 1 + .../model/config/AnalyticsModule.groovy | 9 + .../model/config/LogAnalytics.groovy | 13 ++ .../request/auction/PrebidAnalytics.groovy | 5 +- .../service/PrebidServerService.groovy | 15 ++ .../functional/tests/AnalyticsSpec.groovy | 206 ++++++++++++++++++ .../AnalyticsReporterDelegatorTest.java | 132 ++++++++++- 13 files changed, 528 insertions(+), 14 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy diff --git a/docs/application-settings.md b/docs/application-settings.md index 39fd52c7e5a..bf0dc61bfd0 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -88,6 +88,7 @@ Keep in mind following restrictions: - `analytics.allow-client-details` - when true, this boolean setting allows responses to transmit the server-side analytics tags to support client-side analytics adapters. Defaults to false. - `analytics.auction-events.` - defines which channels are supported by analytics for this account - `analytics.modules..*` - space for `module-name` analytics module specific configuration, may be of any shape +- `analytics.modules..*` - a space for specific data for the analytics adapter, which may include an enabled property to control whether the adapter should be triggered, along with other adapter-specific properties. These will be merged under `ext.prebid.analytics.` in the request. - `metrics.verbosity-level` - defines verbosity level of metrics for this account, overrides `metrics.accounts` application settings configuration. - `cookie-sync.default-limit` - if the "limit" isn't specified in the `/cookie_sync` request, this is what to use - `cookie-sync.max-limit` - if the "limit" is specified in the `/cookie_sync` request, it can't be greater than this diff --git a/docs/config-app.md b/docs/config-app.md index 041c1774830..40a2c42784e 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -429,6 +429,7 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration. ## Analytics +- `analytics.global.adapters` - Names of analytics adapters that will work for each request, except those disabled at the account level. - `analytics.pubstack.enabled` - if equals to `true` the Pubstack analytics module will be enabled. Default value is `false`. - `analytics.pubstack.endpoint` - url for reporting events and fetching configuration. - `analytics.pubstack.scopeid` - defined the scope provided by the Pubstack Support Team. diff --git a/src/main/java/org/prebid/server/analytics/model/AmpEvent.java b/src/main/java/org/prebid/server/analytics/model/AmpEvent.java index cf33545a332..fc3e96c913c 100644 --- a/src/main/java/org/prebid/server/analytics/model/AmpEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/AmpEvent.java @@ -13,7 +13,7 @@ /** * Represents a transaction at /openrtb2/amp endpoint. */ -@Builder +@Builder(toBuilder = true) @Value public class AmpEvent { diff --git a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java index fa2b48746aa..28768d6734f 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java +++ b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java @@ -28,6 +28,7 @@ import org.prebid.server.auction.privacy.enforcement.TcfEnforcement; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -37,6 +38,8 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.StreamUtil; import java.util.Collections; @@ -44,13 +47,11 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -/** - * Class dispatches event processing to all enabled reporters. - */ public class AnalyticsReporterDelegator { private static final Logger logger = LoggerFactory.getLogger(AnalyticsReporterDelegator.class); @@ -63,6 +64,8 @@ public class AnalyticsReporterDelegator { private final UserFpdActivityMask mask; private final Metrics metrics; private final double logSamplingRate; + private final Set globalEnabledAdapters; + private final JacksonMapper mapper; private final Set reporterVendorIds; private final Set reporterNames; @@ -72,7 +75,9 @@ public AnalyticsReporterDelegator(Vertx vertx, TcfEnforcement tcfEnforcement, UserFpdActivityMask userFpdActivityMask, Metrics metrics, - double logSamplingRate) { + double logSamplingRate, + Set globalEnabledAdapters, + JacksonMapper mapper) { this.vertx = Objects.requireNonNull(vertx); this.delegates = Objects.requireNonNull(delegates); @@ -80,6 +85,10 @@ public AnalyticsReporterDelegator(Vertx vertx, this.mask = Objects.requireNonNull(userFpdActivityMask); this.metrics = Objects.requireNonNull(metrics); this.logSamplingRate = logSamplingRate; + this.globalEnabledAdapters = CollectionUtils.isEmpty(globalEnabledAdapters) + ? Collections.emptySet() + : globalEnabledAdapters; + this.mapper = Objects.requireNonNull(mapper); reporterVendorIds = delegates.stream().map(AnalyticsReporter::vendorId).collect(Collectors.toSet()); reporterNames = delegates.stream().map(AnalyticsReporter::name).collect(Collectors.toSet()); @@ -163,11 +172,14 @@ private static boolean isNotEmptyObjectNode(JsonNode analytics) { return analytics != null && analytics.isObject() && !analytics.isEmpty(); } - private static boolean isAllowedAdapter(T event, String adapter) { + private boolean isAllowedAdapter(T event, String adapter) { final ActivityInfrastructure activityInfrastructure; final ActivityInvocationPayload activityInvocationPayload; switch (event) { case AuctionEvent auctionEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, auctionEvent.getAuctionContext())) { + return false; + } final AuctionContext auctionContext = auctionEvent.getAuctionContext(); activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; activityInvocationPayload = auctionContext != null @@ -177,6 +189,10 @@ private static boolean isAllowedAdapter(T event, String adapter) { : null; } case AmpEvent ampEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, ampEvent.getAuctionContext())) { + return false; + } + final AuctionContext auctionContext = ampEvent.getAuctionContext(); activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null; activityInvocationPayload = auctionContext != null @@ -186,9 +202,19 @@ private static boolean isAllowedAdapter(T event, String adapter) { : null; } case NotificationEvent notificationEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, notificationEvent.getAccount())) { + return false; + } activityInfrastructure = notificationEvent.getActivityInfrastructure(); activityInvocationPayload = activityInvocationPayload(adapter); } + case VideoEvent videoEvent -> { + if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, videoEvent.getAuctionContext())) { + return false; + } + activityInfrastructure = null; + activityInvocationPayload = null; + } case null, default -> { activityInfrastructure = null; activityInvocationPayload = null; @@ -198,6 +224,28 @@ private static boolean isAllowedAdapter(T event, String adapter) { return isAllowedActivity(activityInfrastructure, Activity.REPORT_ANALYTICS, activityInvocationPayload); } + private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, AuctionContext auctionContext) { + return isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, + Optional.ofNullable(auctionContext) + .map(AuctionContext::getAccount) + .orElse(null)); + } + + private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, Account account) { + final Map modules = Optional.ofNullable(account) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .orElse(null); + + if (modules != null && modules.containsKey(adapter)) { + final ObjectNode moduleConfig = modules.get(adapter); + return moduleConfig == null || !moduleConfig.has("enabled") + || !moduleConfig.get("enabled").asBoolean(); + } + + return !globalEnabledAdapters.contains(adapter); + } + private static ActivityInvocationPayload activityInvocationPayload(String adapterName) { return ActivityInvocationPayloadImpl.of(ComponentType.ANALYTICS, adapterName); } @@ -299,7 +347,8 @@ private static ObjectNode prepareAnalytics(ObjectNode analytics, String adapterN private void processEventByReporter(AnalyticsReporter analyticsReporter, T event) { final String reporterName = analyticsReporter.name(); - analyticsReporter.processEvent(event) + + analyticsReporter.processEvent(updateEventIfRequired(event, analyticsReporter.name())) .map(ignored -> processSuccess(event, reporterName)) .otherwise(exception -> processFail(exception, event, reporterName)); } @@ -335,4 +384,89 @@ private void updateMetricsByEventType(T event, String analyticsCode, MetricN metrics.updateAnalyticEventMetric(analyticsCode, eventType, result); } + + private T updateEventIfRequired(T event, String adapter) { + switch (event) { + case AuctionEvent auctionEvent -> { + final AuctionContext auctionContext = updateAuctionContext(auctionEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) auctionEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case AmpEvent ampEvent -> { + final AuctionContext auctionContext = updateAuctionContext(ampEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) ampEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case VideoEvent videoEvent -> { + final AuctionContext auctionContext = updateAuctionContext(videoEvent.getAuctionContext(), adapter); + return auctionContext != null + ? (T) videoEvent.toBuilder().auctionContext(auctionContext).build() + : event; + } + case null, default -> { + return event; + } + } + } + + private AuctionContext updateAuctionContext(AuctionContext context, String adapterName) { + final Map modules = Optional.ofNullable(context) + .map(AuctionContext::getAccount) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .orElse(null); + + if (modules != null && modules.containsKey(adapterName)) { + final ObjectNode moduleConfig = modules.get(adapterName); + if (moduleConfigContainsAdapterSpecificData(moduleConfig)) { + final JsonNode analyticsNode = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .orElse(null); + + if (analyticsNode != null && analyticsNode.isObject()) { + final ObjectNode adapterNode = Optional.ofNullable((ObjectNode) analyticsNode.get(adapterName)) + .orElse(mapper.mapper().createObjectNode()); + + moduleConfig.fields().forEachRemaining(entry -> { + final String fieldName = entry.getKey(); + if (!"enabled".equals(fieldName) && !adapterNode.has(fieldName)) { + adapterNode.set(fieldName, entry.getValue()); + } + }); + + ((ObjectNode) analyticsNode).set(adapterName, adapterNode); + final ExtRequestPrebid updatedPrebid = ExtRequestPrebid.builder() + .analytics(analyticsNode) + .build(); + final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); + final BidRequest updatedBidRequest = context.getBidRequest().toBuilder() + .ext(updatedExtRequest) + .build(); + return context.toBuilder() + .bidRequest(updatedBidRequest) + .build(); + } + } + } + + return null; + } + + private boolean moduleConfigContainsAdapterSpecificData(ObjectNode moduleConfig) { + if (moduleConfig != null) { + final Iterator fieldNames = moduleConfig.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (!"enabled".equals(fieldName)) { + return true; + } + } + } + + return false; + } } diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java index ea8b68d0ebc..01153008824 100644 --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java @@ -32,6 +32,7 @@ import jakarta.validation.constraints.NotNull; import java.time.Clock; import java.util.List; +import java.util.Set; import java.util.Map; import java.util.stream.Collectors; @@ -45,7 +46,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( TcfEnforcement tcfEnforcement, UserFpdActivityMask userFpdActivityMask, Metrics metrics, - @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + @Value("${logging.sampling-rate:0.01}") double logSamplingRate, + @Value("${analytics.global.adapters}") Set globalEnabledAdapters, + JacksonMapper mapper) { return new AnalyticsReporterDelegator( vertx, @@ -53,7 +56,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( tcfEnforcement, userFpdActivityMask, metrics, - logSamplingRate); + logSamplingRate, + globalEnabledAdapters, + mapper); } @Bean diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index c9e1eb589e3..eb73cc16478 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -283,6 +283,8 @@ ipv6: anon-left-mask-bits: 56 private-networks: ::1/128, 2001:db8::/32, fc00::/7, fe80::/10, ff00::/8 analytics: + global: + adapters: logAnalytics, pubstack, greenbids, agmaAnalytics pubstack: enabled: false endpoint: http://localhost:8090 diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy index 017058e0b90..0a15cead562 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAnalyticsConfig.groovy @@ -11,6 +11,7 @@ class AccountAnalyticsConfig { Map auctionEvents Boolean allowClientDetails + AnalyticsModule modules @JsonProperty("auction_events") Map auctionEventsSnakeCase diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy new file mode 100644 index 00000000000..5b53fedbe8d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AnalyticsModule.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AnalyticsModule { + + LogAnalytics logAnalytics +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy b/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy new file mode 100644 index 00000000000..cc6d9cc033a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/LogAnalytics.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class LogAnalytics { + + Boolean enabled + String additionalData +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy index 6aa25569030..f6ab5331ca7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidAnalytics.groovy @@ -1,12 +1,11 @@ package org.prebid.server.functional.model.request.auction -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import org.prebid.server.functional.model.config.LogAnalytics -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class PrebidAnalytics { AnalyticsOptions options + LogAnalytics logAnalytics } diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index fd0bcac255c..bac2badd46a 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -379,6 +379,21 @@ class PrebidServerService implements ObjectMapperWrapper { filteredLogs } + String getLogsByValue(String value) { + if (!value) { + throw new IllegalArgumentException("Value is null or empty") + } + getPbsLogsByValue(value) + } + + Boolean isContainLogsByValue(String value) { + getPbsLogsByValue(value) != null + } + + private String getPbsLogsByValue(String value) { + pbsContainer.logs.split("\n").find { it.contains(value) } + } + T getValueFromContainer(String path, Class clazz) { pbsContainer.copyFileFromContainer(path, { inputStream -> return decode(inputStream, clazz) diff --git a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy index c3b6f9e76b0..a16811b8261 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy @@ -1,7 +1,13 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.config.AccountAnalyticsConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AnalyticsModule +import org.prebid.server.functional.model.config.LogAnalytics +import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.mock.services.pubstack.PubStackResponse import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.PrebidAnalytics import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.Dependencies import org.prebid.server.functional.testcontainers.PbsConfig @@ -13,7 +19,15 @@ import spock.lang.Shared class AnalyticsSpec extends BaseSpec { private static final String SCOPE_ID = UUID.randomUUID() + private static final Map ENABLED_DEBUG_LOG_MODE = ["logging.level.root": "debug"] private static final PrebidServerService pbsService = pbsServiceFactory.getService(PbsConfig.getPubstackAnalyticsConfig(SCOPE_ID)) + private static final PrebidServerService pbsServiceWithLogAnalytics = pbsServiceFactory.getService( + ENABLED_DEBUG_LOG_MODE + ['analytics.log.enabled' : 'true', + 'analytics.global.adapters': 'logAnalytics']) + private static final PrebidServerService pbsServiceWithoutLogAnalytics = pbsServiceFactory.getService( + ENABLED_DEBUG_LOG_MODE + ['analytics.log.enabled' : 'true', + 'analytics.global.adapters': '']) + @Shared PubStackAnalytics analytics = new PubStackAnalytics(Dependencies.networkServiceContainer).tap { @@ -34,4 +48,196 @@ class AnalyticsSpec extends BaseSpec { then: "PBS should call pubstack analytics" PBSUtils.waitUntil { analytics.requestCount == analyticsRequestCount + 1 } } + + def "PBS should populate log analytics when logging enabled in global config but not in account config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: null)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS should call log analytics" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert logsByValue + + and: "Analytics adapter shouldn't contain additional info" + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + } + + def "PBS shouldn't populate log analytics when log enabled in account and disabled in global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: true) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS should populate log analytics when log enabled in account and global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: true) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS should call log analytics" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert logsByValue + + and: "Analytics adapter shouldn't contain additional info" + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + } + + def "PBS shouldn't populate log analytics when log disabled in account and enabled in global config"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: false) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS shouldn't populate log analytics when log disabled in global config and without account"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: null)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call log analytics" + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert !logsByValue + } + + def "PBS should populate log analytics with additional data when log enabled in account and data specified"() { + given: "Basic bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics() + } + + and: "Account in the DB" + def additionalData = PBSUtils.randomString + def logAnalyticsModule = new LogAnalytics(enabled: true, additionalData: additionalData) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS should call log analytics" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert logsByValue + + and: "Analytics adapter should contain additional info" + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + } + + def "PBS should populate log analytics with additional data from request when log enabled in account and data specified in request only"() { + given: "Basic bid request" + def additionalData = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics(logAnalytics: new LogAnalytics(additionalData: additionalData)) + } + + and: "Account in the DB" + def logAnalyticsModule = new LogAnalytics(enabled: true, additionalData: null) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS should call log analytics" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert logsByValue + + and: "Analytics adapter should contain additional info" + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + } + + def "PBS should prioritize logAnalytics from request when data specified in account and request"() { + given: "Basic bid request" + def bidRequestAdditionalData = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.analytics = new PrebidAnalytics(logAnalytics: new LogAnalytics(additionalData: bidRequestAdditionalData)) + } + + and: "Account in the DB" + def accountAdditionalData = PBSUtils.randomString + def logAnalyticsModule = new LogAnalytics(enabled: true, additionalData: accountAdditionalData) + def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) + def accountConfig = new AccountConfig(analytics: config) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) + + then: "PBS should call log analytics" + PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) + def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) + assert logsByValue + + and: "Analytics adapter should contain additional info" + def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) + assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == bidRequestAdditionalData + } + + private static BidRequest extractResolvedRequestFromLog(String logsByText) { + decode(logsByText.split("resolvedrequest")[1] + .replace(";", "") + .replaceFirst(":", "") + .replaceFirst("\"", ""), BidRequest.class) + } } diff --git a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java index 35f829a4c38..cdced2f96f3 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java @@ -19,6 +19,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; +import org.prebid.server.VertxTest; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -36,10 +37,13 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -58,7 +62,7 @@ import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) -public class AnalyticsReporterDelegatorTest { +public class AnalyticsReporterDelegatorTest extends VertxTest { private static final String EVENT = StringUtils.EMPTY; private static final Integer FIRST_REPORTER_ID = 1; @@ -100,7 +104,14 @@ public void setUp() { .willReturn(Future.succeededFuture(enforcementActionMap)); target = new AnalyticsReporterDelegator( - vertx, List.of(firstReporter, secondReporter), tcfEnforcement, userFpdActivityMask, metrics, 0.01); + vertx, + List.of(firstReporter, secondReporter), + tcfEnforcement, + userFpdActivityMask, + metrics, + 0.01, + Set.of("logAnalytics", "adapter"), + jacksonMapper); } @Test @@ -387,6 +398,122 @@ public void shouldUpdateAuctionEventToConsideringActivitiesRestrictions() { }); } + @Test + public void shouldNotCallAnalyticsAdapterIfDisabledByAccount() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("enabled", false); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(mapper.createObjectNode()).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx).runOnContext(any()); + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(firstReporter, never()).processEvent(auctionEventCaptor.capture()); + } + + @Test + public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConfig() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("enabled", true); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(mapper.createObjectNode()).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx, times(2)).runOnContext(any()); + + final ObjectNode expectedAnalyticsNode = mapper.createObjectNode(); + final ObjectNode expectedLogAnalyticsNode = mapper.createObjectNode(); + expectedLogAnalyticsNode.put("property1", "value1"); + expectedLogAnalyticsNode.put("property2", "value2"); + expectedAnalyticsNode.set("logAnalytics", expectedLogAnalyticsNode); + + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(firstReporter).processEvent(auctionEventCaptor.capture()); + assertThat(auctionEventCaptor.getValue()) + .extracting(AuctionEvent::getAuctionContext) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getAnalytics) + .isEqualTo(expectedAnalyticsNode); + } + + @Test + public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConfigWithPrecedenceForRequest() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("enabled", true); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final ObjectNode analyticsNode = mapper.createObjectNode(); + final ObjectNode logAnalyticsNode = mapper.createObjectNode(); + logAnalyticsNode.put("property1", "requestValue1"); + logAnalyticsNode.put("property3", "requestValue3"); + analyticsNode.set("logAnalytics", logAnalyticsNode); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(analyticsNode).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx, times(2)).runOnContext(any()); + + final ObjectNode expectedAnalyticsNode = mapper.createObjectNode(); + final ObjectNode expectedLogAnalyticsNode = mapper.createObjectNode(); + expectedLogAnalyticsNode.put("property1", "requestValue1"); + expectedLogAnalyticsNode.put("property2", "value2"); + expectedLogAnalyticsNode.put("property3", "requestValue3"); + expectedAnalyticsNode.set("logAnalytics", expectedLogAnalyticsNode); + + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); + verify(firstReporter).processEvent(auctionEventCaptor.capture()); + assertThat(auctionEventCaptor.getValue()) + .extracting(AuctionEvent::getAuctionContext) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getAnalytics) + .isEqualTo(expectedAnalyticsNode); + } + @SuppressWarnings("unchecked") private static Answer withNullAndInvokeHandler() { return invocation -> { @@ -412,6 +539,7 @@ private static AuctionEvent givenAuctionEvent( return AuctionEvent.builder() .auctionContext(AuctionContext.builder() + .account(Account.builder().build()) .bidRequest(bidRequestCustomizer.apply(BidRequest.builder()).build()) .build()) .build(); From 2e8de3a2fd5e4568da5a49998f4fa48a2bc048ea Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:27:17 +0200 Subject: [PATCH 072/170] Core: Minor Bidders Updates (#3460) --- .../prebid/server/bidder/connectad/ConnectAdBidder.java | 8 ++++---- .../openrtb/ext/request/connectad/ExtImpConnectAd.java | 4 ++-- src/main/resources/bidder-config/lemmadigital.yaml | 8 +++++++- src/main/resources/bidder-config/playdigo.yaml | 2 +- src/main/resources/bidder-config/qt.yaml | 2 +- src/main/resources/bidder-config/smartx.yaml | 1 + .../server/bidder/connectad/ConnectAdBidderTest.java | 6 +++--- .../connectad/test-auction-connectad-request.json | 4 ++-- .../it/openrtb2/connectad/test-connectad-bid-request.json | 4 ++-- .../it/openrtb2/smartx/test-smartx-bid-request.json | 6 ++---- 10 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java index afac2b57f84..4c64992184b 100644 --- a/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java +++ b/src/main/java/org/prebid/server/bidder/connectad/ConnectAdBidder.java @@ -91,18 +91,18 @@ private ExtImpConnectAd parseImpExt(Imp imp) { } catch (IllegalArgumentException e) { throw new PreBidException("Impression id=%s, has invalid Ext".formatted(imp.getId())); } - final Integer siteId = extImpConnectAd.getSiteId(); - if (siteId == null || siteId == 0) { + final String siteId = extImpConnectAd.getSiteId(); + if (siteId == null) { throw new PreBidException("Impression id=%s, has no siteId present".formatted(imp.getId())); } return extImpConnectAd; } - private Imp updateImp(Imp imp, Integer secure, Integer siteId, BigDecimal bidFloor) { + private Imp updateImp(Imp imp, Integer secure, String siteId, BigDecimal bidFloor) { final boolean isValidBidFloor = BidderUtil.isValidPrice(bidFloor); return imp.toBuilder() .banner(updateBanner(imp.getBanner())) - .tagid(siteId.toString()) + .tagid(siteId) .secure(secure) .bidfloor(isValidBidFloor ? bidFloor : imp.getBidfloor()) .bidfloorcur(isValidBidFloor ? "USD" : imp.getBidfloorcur()) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java index fc5f6dc6489..a88bbc2194d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connectad/ExtImpConnectAd.java @@ -11,10 +11,10 @@ public class ExtImpConnectAd { @JsonProperty("networkId") - Integer networkId; + String networkId; @JsonProperty("siteId") - Integer siteId; + String siteId; @JsonProperty("bidfloor") BigDecimal bidFloor; diff --git a/src/main/resources/bidder-config/lemmadigital.yaml b/src/main/resources/bidder-config/lemmadigital.yaml index 1d7208aff4f..3f71b2d016c 100644 --- a/src/main/resources/bidder-config/lemmadigital.yaml +++ b/src/main/resources/bidder-config/lemmadigital.yaml @@ -1,6 +1,6 @@ adapters: lemmadigital: - endpoint: https://sg.ads.lemmatechnologies.com/lemma/servad?pid={{PublisherID}}&aid={{AdUnit}} + endpoint: https://pbid.lemmamedia.com/lemma/servad?src=prebid&pid={{PublisherID}}&aid={{AdUnit}} meta-info: maintainer-email: support@lemmatechnologies.com endpoint-compression: gzip @@ -13,3 +13,9 @@ adapters: - video supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: lemmadigital + redirect: + url: https://sync.lemmadigital.com/setuid?publisher=850&redirect={{redirect_url}} + support-cors: false + uid-macro: '${UUID}' diff --git a/src/main/resources/bidder-config/playdigo.yaml b/src/main/resources/bidder-config/playdigo.yaml index 2c8706860f8..adb14424c1b 100644 --- a/src/main/resources/bidder-config/playdigo.yaml +++ b/src/main/resources/bidder-config/playdigo.yaml @@ -14,7 +14,7 @@ adapters: - video - native supported-vendors: - vendor-id: 0 + vendor-id: 1302 usersync: cookie-family-name: playdigo redirect: diff --git a/src/main/resources/bidder-config/qt.yaml b/src/main/resources/bidder-config/qt.yaml index 67e006405d3..8c81123c8ca 100644 --- a/src/main/resources/bidder-config/qt.yaml +++ b/src/main/resources/bidder-config/qt.yaml @@ -12,7 +12,7 @@ adapters: - video - native supported-vendors: - vendor-id: 0 + vendor-id: 1331 usersync: cookie-family-name: qt redirect: diff --git a/src/main/resources/bidder-config/smartx.yaml b/src/main/resources/bidder-config/smartx.yaml index 50a731d8c9d..d0f4498a32a 100644 --- a/src/main/resources/bidder-config/smartx.yaml +++ b/src/main/resources/bidder-config/smartx.yaml @@ -1,6 +1,7 @@ adapters: smartx: endpoint: https://bid.smartclip.net/bid/1005 + ortb-version: "2.6" meta-info: maintainer-email: bidding@smartclip.tv app-media-types: diff --git a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java index 35e12ad3510..3ac89edc67a 100644 --- a/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/connectad/ConnectAdBidderTest.java @@ -146,7 +146,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtHasNoSiteId() { impBuilder -> impBuilder .id("123") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of(12, null, BigDecimal.ONE))))); + ExtImpConnectAd.of("12", null, BigDecimal.ONE))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -164,7 +164,7 @@ public void impSecureShouldBeOneIfSitePageStartsFromHttps() { impBuilder -> impBuilder .id("123") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of(12, 1, BigDecimal.ONE))))); + ExtImpConnectAd.of("12", "1", BigDecimal.ONE))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -202,7 +202,7 @@ private static Imp givenImp(Function impCustomiz .w(14) .h(15).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpConnectAd.of(12, 12, BigDecimal.ONE))))) + ExtImpConnectAd.of("12", "12", BigDecimal.ONE))))) .build(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json index a5abd2ecf40..00efb79c5a1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-request.json @@ -10,8 +10,8 @@ "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2", "ext": { "connectad": { - "networkId": 12, - "siteId": 15, + "networkId": "12", + "siteId": "15", "bidfloor": 14.7 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json index 7058b025b51..c511c5b8e65 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-connectad-bid-request.json @@ -15,8 +15,8 @@ "ext": { "tid": "${json-unit.any-string}", "bidder": { - "networkId": 12, - "siteId": 15, + "networkId": "12", + "siteId": "15", "bidfloor": 14.7 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-smartx-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-smartx-bid-request.json index d45132bc712..2374f8df992 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-smartx-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-smartx-bid-request.json @@ -40,10 +40,8 @@ "cur": [ "USD" ], - "regs": { - "ext": { - "gdpr": 0 - } + "regs" : { + "gdpr" : 0 }, "ext": { "prebid": { From fe2daa28e51f7dd9a5a52be36ff61b3ee3eaadfb Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:27:46 +0200 Subject: [PATCH 073/170] Adnuntius: DSA Support (#3457) --- .../bidder/adnuntius/AdnuntiusBidder.java | 35 ++++++++++- .../adnuntius/model/response/AdnuntiusAd.java | 2 + .../model/response/AdnuntiusAdvertiser.java | 13 ++++ .../model/response/AdnuntiusBidExt.java | 10 ++++ .../proto/openrtb/ext/response/ExtBidDsa.java | 20 +------ .../bidder/adnuntius/AdnuntiusBidderTest.java | 59 +++++++++++++++++++ 6 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java create mode 100644 src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java index 2de88d8f75c..672f31dd8af 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -19,6 +19,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; @@ -27,7 +28,9 @@ import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBidExt; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusNetBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusResponse; @@ -42,10 +45,12 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; @@ -356,10 +361,10 @@ private List extractBids(BidRequest bidRequest, AdnuntiusResponse adn final String bidType = extImpAdnuntius.getBidType(); currency = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getCurrency); - bids.add(createBid(ad, adsUnit.getHtml(), impId, bidType)); + bids.add(createBid(ad, bidRequest, adsUnit.getHtml(), impId, bidType)); for (AdnuntiusAd deal : ListUtils.emptyIfNull(adsUnit.getDeals())) { - bids.add(createBid(deal, deal.getHtml(), impId, bidType)); + bids.add(createBid(deal, bidRequest, deal.getHtml(), impId, bidType)); } } @@ -374,8 +379,9 @@ private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { return CollectionUtils.isNotEmpty(ads) && ads.getFirst() != null; } - private static Bid createBid(AdnuntiusAd ad, String adm, String impId, String bidType) { + private Bid createBid(AdnuntiusAd ad, BidRequest bidRequest, String adm, String impId, String bidType) { final String adId = ad.getAdId(); + final AdnuntiusBidExt bidExt = prepareBidExt(ad, bidRequest); return Bid.builder() .id(adId) @@ -389,9 +395,32 @@ private static Bid createBid(AdnuntiusAd ad, String adm, String impId, String bi .price(resolvePrice(ad, bidType)) .adm(adm) .adomain(extractDomain(ad.getDestinationUrls())) + .ext(bidExt == null ? null : mapper.mapper().valueToTree(bidExt)) .build(); } + private static AdnuntiusBidExt prepareBidExt(AdnuntiusAd ad, BidRequest bidRequest) { + final ExtRegsDsa extRegsDsa = Optional.ofNullable(bidRequest.getRegs()) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .orElse(null); + + final AdnuntiusAdvertiser advertiser = ad.getAdvertiser(); + + if (advertiser != null && advertiser.getName() != null && extRegsDsa != null) { + final String legalName = ObjectUtils.firstNonNull(advertiser.getLegalName(), advertiser.getName()); + final ExtBidDsa dsa = ExtBidDsa.builder() + .adRender(0) + .paid(legalName) + .behalf(legalName) + .build(); + + return AdnuntiusBidExt.of(dsa); + } + + return null; + } + private static Integer parseMeasure(String measure) { try { return Integer.valueOf(measure); diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java index 47ff2c30f04..88367e172d6 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAd.java @@ -40,4 +40,6 @@ public class AdnuntiusAd { @JsonProperty("destinationUrls") Map destinationUrls; + + AdnuntiusAdvertiser advertiser; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java new file mode 100644 index 00000000000..7c371a9b726 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdvertiser.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AdnuntiusAdvertiser { + + @JsonProperty("legalName") + String legalName; + + String name; +} diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java new file mode 100644 index 00000000000..172a23471be --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusBidExt.java @@ -0,0 +1,10 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; + +@Value(staticConstructor = "of") +public class AdnuntiusBidExt { + + ExtBidDsa dsa; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java index aa24e176303..70683a8a7a7 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidDsa.java @@ -1,36 +1,22 @@ package org.prebid.server.proto.openrtb.ext.response; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Value; import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import java.util.List; -/** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa - */ -@Value(staticConstructor = "of") +@Builder(toBuilder = true) +@Value public class ExtBidDsa { - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.behalf - */ String behalf; - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.paid - */ String paid; - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.transparency[] - */ List transparency; - /** - * Defines the contract for bidresponse.seatbid[i].bid[i].ext.dsa.adrender - */ @JsonProperty("adrender") Integer adRender; - } diff --git a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java index a89a3d7a60a..ac80138bbc0 100644 --- a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java @@ -25,6 +25,7 @@ import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusNetBid; @@ -38,6 +39,7 @@ import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; @@ -750,6 +752,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( .creativeId("creativeId") .lineItemId("lineItemId") .dealId("dealId") + .advertiser(AdnuntiusAdvertiser.of(null, "name")) .destinationUrls(Map.of( "key1", "https://www.domain1.com/uri", "key2", "http://www.domain2.dt/uri")))), @@ -760,6 +763,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( .lineItemId("lineItemId") .dealId("dealId") .html("dealHtml") + .advertiser(AdnuntiusAdvertiser.of("legalName", "name")) .destinationUrls(Map.of( "key1", "https://www.domain1.com/uri", "key2", "http://www.domain2.dt/uri")))))); @@ -785,6 +789,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( assertThat(bid).extracting(Bid::getPrice).isEqualTo(BigDecimal.valueOf(1000)); assertThat(bid).extracting(Bid::getAdomain).asList() .containsExactlyInAnyOrder("domain1.com", "domain2.dt"); + assertThat(bid).extracting(Bid::getExt).isNull(); }); assertThat(bidderBid).extracting(BidderBid::getType).isEqualTo(BidType.banner); assertThat(bidderBid).extracting(BidderBid::getBidCurrency).isEqualTo("USD"); @@ -792,6 +797,60 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( assertThat(result.getErrors()).isEmpty(); } + @Test + public void makeBidsShouldReturnTwoBidWithDsaFromDealsAndAdsWhenAdsAndDealsIsSpecifiedAndDsaReturned() + throws JsonProcessingException { + + // given + final BidderCall httpCall = givenHttpCall(givenAdsUnitWithDealsAndAds( + "auId", + List.of(givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .adId("adId") + .creativeId("creativeId") + .lineItemId("lineItemId") + .dealId("dealId") + .advertiser(AdnuntiusAdvertiser.of(null, "name")) + .destinationUrls(Map.of( + "key1", "https://www.domain1.com/uri", + "key2", "http://www.domain2.dt/uri")))), + List.of(givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .adId("adId") + .creativeId("creativeId") + .lineItemId("lineItemId") + .dealId("dealId") + .html("dealHtml") + .advertiser(AdnuntiusAdvertiser.of("legalName", "name")) + .destinationUrls(Map.of( + "key1", "https://www.domain1.com/uri", + "key2", "http://www.domain2.dt/uri")))))); + + final ExtRegsDsa dsa = ExtRegsDsa.of(1, 0, 2, null); + final BidRequest bidRequest = givenBidRequest( + request -> request.regs(Regs.builder().ext(ExtRegs.of(null, null, null, dsa)).build()), + givenImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).hasSize(2) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly( + mapper.createObjectNode().set("dsa", mapper.createObjectNode() + .put("paid", "name") + .put("behalf", "name") + .put("adrender", 0)), + mapper.createObjectNode().set("dsa", mapper.createObjectNode() + .put("paid", "legalName") + .put("behalf", "legalName") + .put("adrender", 0))); + + assertThat(result.getErrors()).isEmpty(); + } + @Test public void makeBidsShouldReturnErrorIfCreativeHeightOfSomeAdIsAbsent() throws JsonProcessingException { // given From 1e2a4f48e4323a71c07ac564ef85b2d9f116104e Mon Sep 17 00:00:00 2001 From: Rafael Taveira <103446145+rafataveira@users.noreply.github.com> Date: Wed, 25 Sep 2024 05:50:42 -0400 Subject: [PATCH 074/170] Nativo: Fix usersync redirect url (#3462) --- src/main/resources/bidder-config/generic.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index 2c15fd531dd..06ec164dfd9 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -122,7 +122,7 @@ adapters: usersync: cookie-family-name: nativo redirect: - url: http://jadserve.postrelease.com/suid/101787?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&ntv_gpp_consent={{gpp}}&ntv_r={{redirect_url}} + url: https://jadserve.postrelease.com/suid/101787?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&ntv_gpp_consent={{gpp}}&ntv_r={{redirect_url}} support-cors: false uid-macro: 'NTV_USER_ID' meta-info: From e43cf2ad3a468d6e04d3451b279fa23198e33359 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Wed, 25 Sep 2024 14:38:33 +0200 Subject: [PATCH 075/170] Refactoring: Extract code from exchangeService (#3464) --- .../server/auction/ExchangeService.java | 225 +--------------- .../server/auction/HookDebugInfoEnricher.java | 247 ++++++++++++++++++ 2 files changed, 249 insertions(+), 223 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 2dbf1a6528f..87953ff2177 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -75,9 +75,6 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionOutcome; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; -import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.json.JacksonMapper; @@ -109,22 +106,11 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; -import org.prebid.server.proto.openrtb.ext.request.TraceLevel; import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; -import org.prebid.server.proto.openrtb.ext.response.ExtModules; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; -import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; @@ -147,7 +133,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -265,7 +250,7 @@ public Future holdAuction(AuctionContext context) { return processAuctionRequest(context) .compose(this::invokeResponseHooks) .map(this::enrichWithAnalyticsTags) - .map(this::enrichWithHooksDebugInfo) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) .map(this::updateHooksMetrics); } @@ -1610,7 +1595,7 @@ private AuctionContext enrichWithAnalyticsTags(AuctionContext context) { return addClientDetailsWarning(context); } - final List extAnalyticsTags = toExtAnalyticsTags(context); + final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); if (extAnalyticsTags == null) { return context; @@ -1690,212 +1675,6 @@ private static AuctionContext addClientDetailsWarning(AuctionContext context) { return context.with(updatedBidResponse); } - private AuctionContext enrichWithHooksDebugInfo(AuctionContext context) { - final ExtModules extModules = toExtModules(context); - - if (extModules == null) { - return context; - } - - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); - - final ExtBidResponsePrebid updatedExtPrebid = extPrebid - .map(ExtBidResponsePrebid::toBuilder) - .orElse(ExtBidResponsePrebid.builder()) - .modules(extModules) - .build(); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .prebid(updatedExtPrebid) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private static ExtModules toExtModules(AuctionContext context) { - final Map>> errors = - toHookMessages(context, HookExecutionOutcome::getErrors); - final Map>> warnings = - toHookMessages(context, HookExecutionOutcome::getWarnings); - final ExtModulesTrace trace = toHookTrace(context); - return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; - } - - private static Map>> toHookMessages( - AuctionContext context, - Function> messagesGetter) { - - if (!context.getDebugContext().isDebugEnabled()) { - return null; - } - - final Map> hookOutcomesByModule = - context.getHookExecutionContext().getStageOutcomes().values().stream() - .flatMap(Collection::stream) - .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) - .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) - .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) - .collect(Collectors.groupingBy( - hookOutcome -> hookOutcome.getHookId().getModuleCode())); - - final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - outcomes -> outcomes.getValue().stream() - .collect(Collectors.groupingBy( - hookOutcome -> hookOutcome.getHookId().getHookImplCode())) - .entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - messagesLists -> messagesLists.getValue().stream() - .map(messagesGetter) - .flatMap(Collection::stream) - .toList())))); - - return !messagesByModule.isEmpty() ? messagesByModule : null; - } - - private static ExtModulesTrace toHookTrace(AuctionContext context) { - final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); - - if (traceLevel == null) { - return null; - } - - final List stages = context.getHookExecutionContext().getStageOutcomes() - .entrySet().stream() - .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) - .filter(Objects::nonNull) - .toList(); - - if (stages.isEmpty()) { - return null; - } - - final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); - return ExtModulesTrace.of(executionTime, stages); - } - - private static ExtModulesTraceStage toTraceStage(Stage stage, - List stageOutcomes, - TraceLevel level) { - - final List extStageOutcomes = stageOutcomes.stream() - .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) - .filter(Objects::nonNull) - .toList(); - - if (extStageOutcomes.isEmpty()) { - return null; - } - - final long executionTime = extStageOutcomes.stream() - .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) - .max() - .orElse(0L); - - return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); - } - - private static ExtModulesTraceStageOutcome toTraceStageOutcome( - StageExecutionOutcome stageOutcome, TraceLevel level) { - - final List groups = stageOutcome.getGroups().stream() - .map(group -> toTraceGroup(group, level)) - .toList(); - - if (groups.isEmpty()) { - return null; - } - - final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); - return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); - } - - private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { - final List invocationResults = group.getHooks().stream() - .map(hook -> toTraceInvocationResult(hook, level)) - .toList(); - - final long executionTime = invocationResults.stream() - .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) - .max() - .orElse(0L); - - return ExtModulesTraceGroup.of(executionTime, invocationResults); - } - - private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, - TraceLevel level) { - return ExtModulesTraceInvocationResult.builder() - .hookId(hook.getHookId()) - .executionTime(hook.getExecutionTime()) - .status(hook.getStatus()) - .message(hook.getMessage()) - .action(hook.getAction()) - .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) - .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) - .build(); - } - - private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { - if (analyticsTags == null) { - return null; - } - - return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() - .filter(Objects::nonNull) - .map(ExchangeService::toTraceAnalyticsActivity) - .toList()); - } - - private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity( - org.prebid.server.hooks.v1.analytics.Activity activity) { - - return ExtModulesTraceAnalyticsActivity.of( - activity.name(), - activity.status(), - CollectionUtils.emptyIfNull(activity.results()).stream() - .filter(Objects::nonNull) - .map(ExchangeService::toTraceAnalyticsResult) - .toList()); - } - - private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { - final AppliedTo appliedTo = result.appliedTo(); - final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null - ? ExtModulesTraceAnalyticsAppliedTo.builder() - .impIds(appliedTo.impIds()) - .bidders(appliedTo.bidders()) - .request(appliedTo.request() ? Boolean.TRUE : null) - .response(appliedTo.response() ? Boolean.TRUE : null) - .bidIds(appliedTo.bidIds()) - .build() - : null; - - return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); - } - - private static List toExtAnalyticsTags(AuctionContext context) { - return context.getHookExecutionContext().getStageOutcomes().entrySet().stream() - .flatMap(stageToExecutionOutcome -> stageToExecutionOutcome.getValue().stream() - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .filter(hookExecutionOutcome -> hookExecutionOutcome.getAnalyticsTags() != null) - .map(hookExecutionOutcome -> ExtAnalyticsTags.of( - stageToExecutionOutcome.getKey(), - hookExecutionOutcome.getHookId().getModuleCode(), - toTraceAnalyticsTags(hookExecutionOutcome.getAnalyticsTags())))) - .toList(); - } - private AuctionContext updateHooksMetrics(AuctionContext context) { final EnumMap> stageOutcomes = context.getHookExecutionContext().getStageOutcomes(); diff --git a/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java new file mode 100644 index 00000000000..ad8cd86410c --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java @@ -0,0 +1,247 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.request.TraceLevel; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class HookDebugInfoEnricher { + + private HookDebugInfoEnricher() { + } + + public static AuctionContext enrichWithHooksDebugInfo(AuctionContext context) { + final ExtModules extModules = toExtModules(context); + + if (extModules == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .modules(extModules) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static ExtModules toExtModules(AuctionContext context) { + final Map>> errors = + toHookMessages(context, HookExecutionOutcome::getErrors); + final Map>> warnings = + toHookMessages(context, HookExecutionOutcome::getWarnings); + final ExtModulesTrace trace = toHookTrace(context); + return ObjectUtils.anyNotNull(errors, warnings, trace) ? ExtModules.of(errors, warnings, trace) : null; + } + + private static Map>> toHookMessages( + AuctionContext context, + Function> messagesGetter) { + + if (!context.getDebugContext().isDebugEnabled()) { + return null; + } + + final Map> hookOutcomesByModule = + context.getHookExecutionContext().getStageOutcomes().values().stream() + .flatMap(Collection::stream) + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .filter(hookOutcome -> CollectionUtils.isNotEmpty(messagesGetter.apply(hookOutcome))) + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getModuleCode())); + + final Map>> messagesByModule = hookOutcomesByModule.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + outcomes -> outcomes.getValue().stream() + .collect(Collectors.groupingBy( + hookOutcome -> hookOutcome.getHookId().getHookImplCode())) + .entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + messagesLists -> messagesLists.getValue().stream() + .map(messagesGetter) + .flatMap(Collection::stream) + .toList())))); + + return !messagesByModule.isEmpty() ? messagesByModule : null; + } + + private static ExtModulesTrace toHookTrace(AuctionContext context) { + final TraceLevel traceLevel = context.getDebugContext().getTraceLevel(); + + if (traceLevel == null) { + return null; + } + + final List stages = context.getHookExecutionContext().getStageOutcomes() + .entrySet().stream() + .map(stageOutcome -> toTraceStage(stageOutcome.getKey(), stageOutcome.getValue(), traceLevel)) + .filter(Objects::nonNull) + .toList(); + + if (stages.isEmpty()) { + return null; + } + + final long executionTime = stages.stream().mapToLong(ExtModulesTraceStage::getExecutionTime).sum(); + return ExtModulesTrace.of(executionTime, stages); + } + + private static ExtModulesTraceStage toTraceStage(Stage stage, + List stageOutcomes, + TraceLevel level) { + + final List extStageOutcomes = stageOutcomes.stream() + .map(stageOutcome -> toTraceStageOutcome(stageOutcome, level)) + .filter(Objects::nonNull) + .toList(); + + if (extStageOutcomes.isEmpty()) { + return null; + } + + final long executionTime = extStageOutcomes.stream() + .mapToLong(ExtModulesTraceStageOutcome::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceStage.of(stage, executionTime, extStageOutcomes); + } + + private static ExtModulesTraceStageOutcome toTraceStageOutcome( + StageExecutionOutcome stageOutcome, TraceLevel level) { + + final List groups = stageOutcome.getGroups().stream() + .map(group -> toTraceGroup(group, level)) + .toList(); + + if (groups.isEmpty()) { + return null; + } + + final long executionTime = groups.stream().mapToLong(ExtModulesTraceGroup::getExecutionTime).sum(); + return ExtModulesTraceStageOutcome.of(stageOutcome.getEntity(), executionTime, groups); + } + + private static ExtModulesTraceGroup toTraceGroup(GroupExecutionOutcome group, TraceLevel level) { + final List invocationResults = group.getHooks().stream() + .map(hook -> toTraceInvocationResult(hook, level)) + .toList(); + + final long executionTime = invocationResults.stream() + .mapToLong(ExtModulesTraceInvocationResult::getExecutionTime) + .max() + .orElse(0L); + + return ExtModulesTraceGroup.of(executionTime, invocationResults); + } + + private static ExtModulesTraceInvocationResult toTraceInvocationResult(HookExecutionOutcome hook, + TraceLevel level) { + return ExtModulesTraceInvocationResult.builder() + .hookId(hook.getHookId()) + .executionTime(hook.getExecutionTime()) + .status(hook.getStatus()) + .message(hook.getMessage()) + .action(hook.getAction()) + .debugMessages(level == TraceLevel.verbose ? hook.getDebugMessages() : null) + .analyticsTags(level == TraceLevel.verbose ? toTraceAnalyticsTags(hook.getAnalyticsTags()) : null) + .build(); + } + + private static ExtModulesTraceAnalyticsTags toTraceAnalyticsTags(Tags analyticsTags) { + if (analyticsTags == null) { + return null; + } + + return ExtModulesTraceAnalyticsTags.of(CollectionUtils.emptyIfNull(analyticsTags.activities()).stream() + .filter(Objects::nonNull) + .map(HookDebugInfoEnricher::toTraceAnalyticsActivity) + .toList()); + } + + private static ExtModulesTraceAnalyticsActivity toTraceAnalyticsActivity( + org.prebid.server.hooks.v1.analytics.Activity activity) { + + return ExtModulesTraceAnalyticsActivity.of( + activity.name(), + activity.status(), + CollectionUtils.emptyIfNull(activity.results()).stream() + .filter(Objects::nonNull) + .map(HookDebugInfoEnricher::toTraceAnalyticsResult) + .toList()); + } + + private static ExtModulesTraceAnalyticsResult toTraceAnalyticsResult(Result result) { + final AppliedTo appliedTo = result.appliedTo(); + final ExtModulesTraceAnalyticsAppliedTo extAppliedTo = appliedTo != null + ? ExtModulesTraceAnalyticsAppliedTo.builder() + .impIds(appliedTo.impIds()) + .bidders(appliedTo.bidders()) + .request(appliedTo.request() ? Boolean.TRUE : null) + .response(appliedTo.response() ? Boolean.TRUE : null) + .bidIds(appliedTo.bidIds()) + .build() + : null; + + return ExtModulesTraceAnalyticsResult.of(result.status(), result.values(), extAppliedTo); + } + + public static List toExtAnalyticsTags(AuctionContext context) { + return context.getHookExecutionContext().getStageOutcomes().entrySet().stream() + .flatMap(stageToExecutionOutcome -> stageToExecutionOutcome.getValue().stream() + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookExecutionOutcome -> hookExecutionOutcome.getAnalyticsTags() != null) + .map(hookExecutionOutcome -> ExtAnalyticsTags.of( + stageToExecutionOutcome.getKey(), + hookExecutionOutcome.getHookId().getModuleCode(), + toTraceAnalyticsTags(hookExecutionOutcome.getAnalyticsTags())))) + .toList(); + } +} From ed717af24c39a5cf17fb6cdde74d21bb58c55983 Mon Sep 17 00:00:00 2001 From: Irakli Gotsiridze Date: Mon, 30 Sep 2024 12:01:21 +0400 Subject: [PATCH 076/170] Sovrn: Enable Gzip compression (#3475) --- src/main/resources/bidder-config/sovrn.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/bidder-config/sovrn.yaml b/src/main/resources/bidder-config/sovrn.yaml index bad733681a6..8f74ae266b6 100644 --- a/src/main/resources/bidder-config/sovrn.yaml +++ b/src/main/resources/bidder-config/sovrn.yaml @@ -1,6 +1,7 @@ adapters: sovrn: endpoint: http://ap.lijit.com/rtb/bid?src=prebid_server + endpoint-compression: gzip modifying-vast-xml-allowed: true meta-info: maintainer-email: sovrnoss@sovrn.com From f5526128fa1ed4e608b04a3fca70876f8e91c581 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Mon, 30 Sep 2024 10:07:22 +0200 Subject: [PATCH 077/170] Refactoring: Extract bidAdjustments code from exchangeService (#3476) --- .../server/auction/AnalyticsTagsEnricher.java | 125 ++ .../prebid/server/auction/BidsAdjuster.java | 241 ++++ .../server/auction/ExchangeService.java | 339 +----- .../spring/config/ServiceConfiguration.java | 30 +- .../java/org/prebid/server/util/ListUtil.java | 5 + .../java/org/prebid/server/util/PbsUtil.java | 16 + .../server/auction/BidsAdjusterTest.java | 1010 +++++++++++++++++ .../server/auction/ExchangeServiceTest.java | 843 +------------- 8 files changed, 1468 insertions(+), 1141 deletions(-) create mode 100644 src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java create mode 100644 src/main/java/org/prebid/server/auction/BidsAdjuster.java create mode 100644 src/main/java/org/prebid/server/util/PbsUtil.java create mode 100644 src/test/java/org/prebid/server/auction/BidsAdjusterTest.java diff --git a/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java new file mode 100644 index 00000000000..15344b28576 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java @@ -0,0 +1,125 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class AnalyticsTagsEnricher { + + private AnalyticsTagsEnricher() { + } + + public static AuctionContext enrichWithAnalyticsTags(AuctionContext context) { + final boolean clientDetailsEnabled = isClientDetailsEnabled(context); + if (!clientDetailsEnabled) { + return context; + } + + final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::isAllowClientDetails) + .orElse(false); + + if (!allowClientDetails) { + return addClientDetailsWarning(context); + } + + final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); + + if (extAnalyticsTags == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .analytics(ExtAnalytics.of(extAnalyticsTags)) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static boolean isClientDetailsEnabled(AuctionContext context) { + final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .orElse(null); + + if (notObjectNode(analytics)) { + return false; + } + + final JsonNode options = analytics.get("options"); + if (notObjectNode(options)) { + return false; + } + + final JsonNode enableClientDetails = options.get("enableclientdetails"); + return enableClientDetails != null + && enableClientDetails.isBoolean() + && enableClientDetails.asBoolean(); + } + + private static boolean notObjectNode(JsonNode jsonNode) { + return jsonNode == null || !jsonNode.isObject(); + } + + private static AuctionContext addClientDetailsWarning(AuctionContext context) { + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + + final Map> warnings = ext + .map(ExtBidResponse::getWarnings) + .orElse(Collections.emptyMap()); + final List prebidWarnings = ObjectUtils.defaultIfNull( + warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), + Collections.emptyList()); + + final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); + updatedPrebidWarnings.add(ExtBidderError.of( + BidderError.Type.generic.getCode(), + "analytics.options.enableclientdetails not enabled for account")); + final Map> updatedWarnings = new HashMap<>(warnings); + updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .warnings(updatedWarnings) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } +} diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java new file mode 100644 index 00000000000..ec6b1c9d2f3 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -0,0 +1,241 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.PbsUtil; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidsAdjuster { + + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; + + private final ResponseBidValidator responseBidValidator; + private final CurrencyConversionService currencyService; + private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final PriceFloorEnforcer priceFloorEnforcer; + private final DsaEnforcer dsaEnforcer; + private final JacksonMapper mapper; + + public BidsAdjuster(ResponseBidValidator responseBidValidator, + CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + JacksonMapper mapper) { + + this.responseBidValidator = Objects.requireNonNull(responseBidValidator); + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); + this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); + this.mapper = Objects.requireNonNull(mapper); + } + + public List validateAndAdjustBids(List auctionParticipations, + AuctionContext auctionContext, + BidderAliases aliases) { + + return auctionParticipations.stream() + .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) + .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) + .map(auctionParticipation -> priceFloorEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getAccount(), + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .map(auctionParticipation -> dsaEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .toList(); + } + + private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, + AuctionContext auctionContext, + BidderAliases aliases) { + + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + final List errors = new ArrayList<>(seatBid.getErrors()); + final List warnings = new ArrayList<>(seatBid.getWarnings()); + + final List requestCurrencies = bidRequest.getCur(); + if (requestCurrencies.size() > 1) { + errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" + .formatted(requestCurrencies.getFirst()))); + } + + final List bids = seatBid.getBids(); + final List validBids = new ArrayList<>(bids.size()); + + for (final BidderBid bid : bids) { + final ValidationResult validationResult = responseBidValidator.validate( + bid, + bidderResponse.getBidder(), + auctionContext, + aliases); + + if (validationResult.hasWarnings() || validationResult.hasErrors()) { + errors.add(makeValidationBidderError(bid.getBid(), validationResult)); + } + + if (!validationResult.hasErrors()) { + validBids.add(bid); + } + } + + final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() + ? bidderResponse + : bidderResponse.with( + seatBid.toBuilder() + .bids(validBids) + .errors(errors) + .warnings(warnings) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { + final String validationErrors = Stream.concat( + validationResult.getErrors().stream().map(message -> "Error: " + message), + validationResult.getWarnings().stream().map(message -> "Warning: " + message)) + .collect(Collectors.joining(". ")); + + final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); + return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); + } + + private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, + BidRequest bidRequest) { + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + + final List bidderBids = seatBid.getBids(); + if (bidderBids.isEmpty()) { + return auctionParticipation; + } + + final List updatedBidderBids = new ArrayList<>(bidderBids.size()); + final List errors = new ArrayList<>(seatBid.getErrors()); + final String adServerCurrency = bidRequest.getCur().getFirst(); + + for (final BidderBid bidderBid : bidderBids) { + try { + final BidderBid updatedBidderBid = + updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); + updatedBidderBids.add(updatedBidderBid); + } catch (PreBidException e) { + errors.add(BidderError.generic(e.getMessage())); + } + } + + final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() + .bids(updatedBidderBids) + .errors(errors) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, + BidderResponse bidderResponse, + BidRequest bidRequest, + String adServerCurrency) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( + price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); + + final BigDecimal priceAdjustmentFactor = + bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); + + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); + + final Bid.BidBuilder bidBuilder = bid.toBuilder(); + if (adjustedPrice.compareTo(price) != 0) { + bidBuilder.price(adjustedPrice); + } + + if (!updatedBidExt.isEmpty()) { + bidBuilder.ext(updatedBidExt); + } + + return bidderBid.toBuilder().bid(bidBuilder.build()).build(); + } + + private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { + final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); + if (adjustmentFactors == null) { + return null; + } + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); + + return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); + } + + private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); + return prebid != null ? prebid.getBidadjustmentfactors() : null; + } + + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { + return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 + ? price.multiply(priceAdjustmentFactor) + : price; + } + + private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); + if (StringUtils.isNotBlank(bidCurrency)) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); + } + } + + private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { + node.set(propertyName, propertyValue); + } +} diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 87953ff2177..66b34a8df46 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -2,9 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; @@ -31,7 +29,6 @@ import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -58,13 +55,11 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.ExecutionAction; @@ -94,7 +89,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; @@ -105,19 +99,11 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; -import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; -import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; -import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.ListUtil; +import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; import java.time.Clock; @@ -134,7 +120,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; public class ExchangeService { @@ -144,8 +129,6 @@ public class ExchangeService { private static final String PREBID_EXT = "prebid"; private static final String BIDDER_EXT = "bidder"; private static final String TID_EXT = "tid"; - private static final String ORIGINAL_BID_CPM = "origbidcpm"; - private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; private static final String ALL_BIDDERS_CONFIG = "*"; private static final Integer DEFAULT_MULTIBID_LIMIT_MIN = 1; private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; @@ -166,17 +149,13 @@ public class ExchangeService { private final TimeoutFactory timeoutFactory; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final HttpBidderRequester httpBidderRequester; - private final ResponseBidValidator responseBidValidator; - private final CurrencyConversionService currencyService; private final BidResponseCreator bidResponseCreator; private final BidResponsePostProcessor bidResponsePostProcessor; private final HookStageExecutor hookStageExecutor; private final HttpInteractionLogger httpInteractionLogger; private final PriceFloorAdjuster priceFloorAdjuster; - private final PriceFloorEnforcer priceFloorEnforcer; private final PriceFloorProcessor priceFloorProcessor; - private final DsaEnforcer dsaEnforcer; - private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final BidsAdjuster bidsAdjuster; private final Metrics metrics; private final Clock clock; private final JacksonMapper mapper; @@ -197,17 +176,13 @@ public ExchangeService(double logSamplingRate, TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, PriceFloorProcessor priceFloorProcessor, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -228,17 +203,13 @@ public ExchangeService(double logSamplingRate, this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.httpBidderRequester = Objects.requireNonNull(httpBidderRequester); - this.responseBidValidator = Objects.requireNonNull(responseBidValidator); - this.currencyService = Objects.requireNonNull(currencyService); this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator); this.bidResponsePostProcessor = Objects.requireNonNull(bidResponsePostProcessor); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); - this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); - this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); - this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.bidsAdjuster = Objects.requireNonNull(bidsAdjuster); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -249,7 +220,7 @@ public ExchangeService(double logSamplingRate, public Future holdAuction(AuctionContext context) { return processAuctionRequest(context) .compose(this::invokeResponseHooks) - .map(this::enrichWithAnalyticsTags) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) .map(this::updateHooksMetrics); } @@ -303,7 +274,8 @@ private Future runAuction(AuctionContext receivedContext) { bidRequest.getImp(), context.getBidRejectionTrackers())) .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings)) - .map(auctionParticipations -> validateAndAdjustBids(auctionParticipations, context, aliases)) + .map(auctionParticipations -> + bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases)) .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases)) .map(context::with)) // produce response from bidder results @@ -319,20 +291,20 @@ private Future runAuction(AuctionContext receivedContext) { } private BidderAliases aliases(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final Map aliases = prebid != null ? prebid.getAliases() : null; final Map aliasgvlids = prebid != null ? prebid.getAliasgvlids() : null; return BidderAliases.of(aliases, aliasgvlids, bidderCatalog); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); return prebid != null ? prebid.getTargeting() : null; } private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { final ExtRequestTargeting targeting = targeting(bidRequest); - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; if (targeting != null && cache != null) { @@ -367,13 +339,8 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { return BidRequestCacheInfo.noCache(); } - private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { - final ExtRequest requestExt = bidRequest.getExt(); - return requestExt != null ? requestExt.getPrebid() : null; - } - private static Map bidderToMultiBids(BidRequest bidRequest, List debugWarnings) { - final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid extRequestPrebid = PbsUtil.extRequestPrebid(bidRequest); final Collection multiBids = extRequestPrebid != null ? CollectionUtils.emptyIfNull(extRequestPrebid.getMultibid()) : Collections.emptyList(); @@ -629,7 +596,7 @@ private User prepareUser(String bidder, userBuilder.buyeruid(buyerUidUpdateResult.getValue()); if (shouldUpdateUserEids) { - userBuilder.eids(nullIfEmpty(allowedUserEids)); + userBuilder.eids(ListUtil.nullIfEmpty(allowedUserEids)); } if (shouldUpdateUserExt) { @@ -706,7 +673,7 @@ private List getAuctionParticipation( * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ private static Map bidderToPrebidBidders(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); if (bidders == null || bidders.isNull()) { @@ -1338,185 +1305,6 @@ private boolean isZeroNonDealBids(BigDecimal price, String dealId) { || (price.compareTo(BigDecimal.ZERO) == 0 && StringUtils.isBlank(dealId)); } - private List validateAndAdjustBids(List auctionParticipations, - AuctionContext auctionContext, - BidderAliases aliases) { - - return auctionParticipations.stream() - .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) - .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) - .map(auctionParticipation -> priceFloorEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getAccount(), - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .map(auctionParticipation -> dsaEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .toList(); - } - - private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, - AuctionContext auctionContext, - BidderAliases aliases) { - - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidRequest bidRequest = auctionContext.getBidRequest(); - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final List errors = new ArrayList<>(seatBid.getErrors()); - final List warnings = new ArrayList<>(seatBid.getWarnings()); - - final List requestCurrencies = bidRequest.getCur(); - if (requestCurrencies.size() > 1) { - errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" - .formatted(requestCurrencies.getFirst()))); - } - - final List bids = seatBid.getBids(); - final List validBids = new ArrayList<>(bids.size()); - - for (final BidderBid bid : bids) { - final ValidationResult validationResult = responseBidValidator.validate( - bid, - bidderResponse.getBidder(), - auctionContext, - aliases); - - if (validationResult.hasWarnings() || validationResult.hasErrors()) { - errors.add(makeValidationBidderError(bid.getBid(), validationResult)); - } - - if (!validationResult.hasErrors()) { - validBids.add(bid); - } - } - - final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() - ? bidderResponse - : bidderResponse.with( - seatBid.toBuilder() - .bids(validBids) - .errors(errors) - .warnings(warnings) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { - final String validationErrors = Stream.concat( - validationResult.getErrors().stream().map(message -> "Error: " + message), - validationResult.getWarnings().stream().map(message -> "Warning: " + message)) - .collect(Collectors.joining(". ")); - - final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); - return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); - } - - private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, - BidRequest bidRequest) { - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - - final List bidderBids = seatBid.getBids(); - if (bidderBids.isEmpty()) { - return auctionParticipation; - } - - final List updatedBidderBids = new ArrayList<>(bidderBids.size()); - final List errors = new ArrayList<>(seatBid.getErrors()); - final String adServerCurrency = bidRequest.getCur().getFirst(); - - for (final BidderBid bidderBid : bidderBids) { - try { - final BidderBid updatedBidderBid = - updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); - updatedBidderBids.add(updatedBidderBid); - } catch (PreBidException e) { - errors.add(BidderError.generic(e.getMessage())); - } - } - - final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() - .bids(updatedBidderBids) - .errors(errors) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, - BidderResponse bidderResponse, - BidRequest bidRequest, - String adServerCurrency) { - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidderBid.getBidCurrency(); - final BigDecimal price = bid.getPrice(); - - final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); - - final BigDecimal priceAdjustmentFactor = - bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); - final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); - - final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); - - updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); - - final Bid.BidBuilder bidBuilder = bid.toBuilder(); - if (adjustedPrice.compareTo(price) != 0) { - bidBuilder.price(adjustedPrice); - } - - if (!updatedBidExt.isEmpty()) { - bidBuilder.ext(updatedBidExt); - } - - return bidderBid.toBuilder().bid(bidBuilder.build()).build(); - } - - private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { - final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); - - return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); - } - - private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - return prebid != null ? prebid.getBidadjustmentfactors() : null; - } - - private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { - return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 - ? price.multiply(priceAdjustmentFactor) - : price; - } - - private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); - if (StringUtils.isNotBlank(bidCurrency)) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); - } - } - - private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { - node.set(propertyName, propertyValue); - } - private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } @@ -1580,101 +1368,6 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { }; } - private AuctionContext enrichWithAnalyticsTags(AuctionContext context) { - final boolean clientDetailsEnabled = isClientDetailsEnabled(context); - if (!clientDetailsEnabled) { - return context; - } - - final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) - .map(Account::getAnalytics) - .map(AccountAnalyticsConfig::isAllowClientDetails) - .orElse(false); - - if (!allowClientDetails) { - return addClientDetailsWarning(context); - } - - final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); - - if (extAnalyticsTags == null) { - return context; - } - - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); - - final ExtBidResponsePrebid updatedExtPrebid = extPrebid - .map(ExtBidResponsePrebid::toBuilder) - .orElse(ExtBidResponsePrebid.builder()) - .analytics(ExtAnalytics.of(extAnalyticsTags)) - .build(); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .prebid(updatedExtPrebid) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private static boolean isClientDetailsEnabled(AuctionContext context) { - final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) - .map(BidRequest::getExt) - .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getAnalytics) - .orElse(null); - - if (notObjectNode(analytics)) { - return false; - } - - final JsonNode options = analytics.get("options"); - if (notObjectNode(options)) { - return false; - } - - final JsonNode enableClientDetails = options.get("enableclientdetails"); - return enableClientDetails != null - && enableClientDetails.isBoolean() - && enableClientDetails.asBoolean(); - } - - private static boolean notObjectNode(JsonNode jsonNode) { - return jsonNode == null || !jsonNode.isObject(); - } - - private static AuctionContext addClientDetailsWarning(AuctionContext context) { - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - - final Map> warnings = ext - .map(ExtBidResponse::getWarnings) - .orElse(Collections.emptyMap()); - final List prebidWarnings = ObjectUtils.defaultIfNull( - warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), - Collections.emptyList()); - - final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); - updatedPrebidWarnings.add(ExtBidderError.of( - BidderError.Type.generic.getCode(), - "analytics.options.enableclientdetails not enabled for account")); - final Map> updatedWarnings = new HashMap<>(warnings); - updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .warnings(updatedWarnings) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - private AuctionContext updateHooksMetrics(AuctionContext context) { final EnumMap> stageOutcomes = context.getHookExecutionContext().getStageOutcomes(); @@ -1727,8 +1420,4 @@ private void updateHookInvocationMetrics(Account account, Stage stage, HookExecu metrics.updateAccountHooksMetrics(account, moduleCode, status, action); } } - - private List nullIfEmpty(List value) { - return CollectionUtils.isEmpty(value) ? null : value; - } } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 8e21fe77b4b..d248264a59d 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -13,6 +13,7 @@ import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.BidResponseCreator; import org.prebid.server.auction.BidResponsePostProcessor; +import org.prebid.server.auction.BidsAdjuster; import org.prebid.server.auction.DebugResolver; import org.prebid.server.auction.DsaEnforcer; import org.prebid.server.auction.ExchangeService; @@ -835,17 +836,13 @@ ExchangeService exchangeService( TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyConversionService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, PriceFloorProcessor priceFloorProcessor, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -867,23 +864,36 @@ ExchangeService exchangeService( timeoutFactory, bidRequestOrtbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyConversionService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, priceFloorProcessor, - dsaEnforcer, - bidAdjustmentFactorResolver, + bidsAdjuster, metrics, clock, mapper, criteriaLogManager, enabledStrictAppSiteDoohValidation); } + @Bean + BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator, + CurrencyConversionService currencyConversionService, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + JacksonMapper mapper) { + + return new BidsAdjuster( + responseBidValidator, + currencyConversionService, + bidAdjustmentFactorResolver, + priceFloorEnforcer, + dsaEnforcer, + mapper); + } + @Bean StoredRequestProcessor storedRequestProcessor( @Value("${auction.stored-requests-timeout-ms}") long defaultTimeoutMs, diff --git a/src/main/java/org/prebid/server/util/ListUtil.java b/src/main/java/org/prebid/server/util/ListUtil.java index e31aaa453ad..66efeaa1858 100644 --- a/src/main/java/org/prebid/server/util/ListUtil.java +++ b/src/main/java/org/prebid/server/util/ListUtil.java @@ -1,5 +1,6 @@ package org.prebid.server.util; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.util.algorithms.ListsUnionView; import java.util.List; @@ -12,4 +13,8 @@ private ListUtil() { public static List union(List first, List second) { return new ListsUnionView<>(first, second); } + + public static List nullIfEmpty(List value) { + return CollectionUtils.isEmpty(value) ? null : value; + } } diff --git a/src/main/java/org/prebid/server/util/PbsUtil.java b/src/main/java/org/prebid/server/util/PbsUtil.java new file mode 100644 index 00000000000..bc81f1ed2b5 --- /dev/null +++ b/src/main/java/org/prebid/server/util/PbsUtil.java @@ -0,0 +1,16 @@ +package org.prebid.server.util; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +public class PbsUtil { + + private PbsUtil() { + } + + public static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + return requestExt != null ? requestExt.getPrebid() : null; + } +} diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java new file mode 100644 index 00000000000..2691b629544 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -0,0 +1,1010 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +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.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.math.BigDecimal; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +@ExtendWith(MockitoExtension.class) +public class BidsAdjusterTest extends VertxTest { + + @Mock(strictness = LENIENT) + private ResponseBidValidator responseBidValidator; + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyService; + + @Mock(strictness = LENIENT) + private PriceFloorEnforcer priceFloorEnforcer; + + @Mock(strictness = LENIENT) + private DsaEnforcer dsaEnforcer; + + @Mock(strictness = LENIENT) + private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + + private BidsAdjuster target; + + @BeforeEach + public void setUp() { + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + + given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); + given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); + given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null); + + givenTarget(); + } + + private void givenTarget() { + target = new BidsAdjuster( + responseBidValidator, + currencyService, + bidAdjustmentFactorResolver, + priceFloorEnforcer, + dsaEnforcer, + jacksonMapper); + } + + @Test + public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(updatedPrice); + } + + @Test + public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2.0)); + } + + @Test + public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD"); + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).isEmpty(); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.TEN); + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willReturn(BigDecimal.TEN); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + final BigDecimal updatedPrice = BigDecimal.valueOf(100); + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()) + .extracting(BidderBid::getBid) + .flatExtracting(Bid::getPrice) + .containsOnly(updatedPrice); + assertThat(firstSeatBid.getErrors()).isEmpty(); + } + + @Test + public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) + .willThrow( + new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); + + assertThat(result).hasSize(1); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "CUR1"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1"); + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); + + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR") + )) + .build(), + 1); + + final BidRequest bidRequest = BidRequest.builder() + .cur(singletonList("BAD")) + .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), + identity()))).build(); + + final BigDecimal updatedPrice = BigDecimal.valueOf(20); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD"))) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD")); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD")); + + assertThat(result).hasSize(1); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "USD"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD"); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsOnly(expectedBidderBid); + + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD"); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCurrency() { + // given + final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(bidderPrice).build(), "USD") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2"))); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1")); + + assertThat(result).hasSize(1); + + final BidderError expectedError = BidderError.badInput("Cur parameter contains more than one currency." + + " CUR1 will be used"); + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()) + .extracting(BidderBid::getBid) + .flatExtracting(Bid::getPrice) + .containsOnly(updatedPrice); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { + // given + final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); + final BigDecimal bidder2Price = BigDecimal.valueOf(2); + final BigDecimal bidder3Price = BigDecimal.valueOf(3); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"), + givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"), + givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(Map.of("bidder1", 1), identity())), + builder -> builder.cur(singletonList("USD"))); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); + verifyNoMoreInteractions(currencyService); + + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsOnly(bidder3Price, updatedPrice, updatedPrice); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(2.468)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(4.936)); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(1).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); + } + + @Test + public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD") + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + } + + @Test + public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.ONE).build(), + "USD") + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .auctiontimestamp(1000L) + .currency(ExtRequestCurrency.of(null, false)) + .bidadjustmentfactors(givenAdjustments) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.ONE); + } + + @Test + public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { + // given + final BidderBid bidToAccept = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderBid bidToReject = + givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.TEN).build(), "USD"); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(bidToAccept, bidToReject)) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(List.of( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), + identity()); + + given(priceFloorEnforcer.enforce(any(), any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(bidToAccept); + } + + @Test + public void shouldReturnBidsAcceptedByDsaEnforcer() { + // given + final BidderBid bidToAccept = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderBid bidToReject = + givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.TEN).build(), "USD"); + + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(bidToAccept, bidToReject)) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(List.of( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), + identity()); + + given(dsaEnforcer.enforce(any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(bidToAccept); + } + + @Test + public void shouldTolerateResponseBidValidationErrors() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(givenBidderBid( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + when(responseBidValidator.validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.error("Error: bid validation error.")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .isEmpty(); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly( + BidderError.invalidBid( + "BidId `bidId1` validation messages: Error: Error: bid validation error.")); + } + + @Test + public void shouldTolerateResponseBidValidationWarnings() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(givenBidderBid( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + when(responseBidValidator.validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.warning(singletonList("Error: bid validation warning."))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .hasSize(1); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly(BidderError.invalidBid( + "BidId `bidId1` validation messages: Warning: Error: bid validation warning.")); + } + + private BidderResponse givenBidderResponse(Bid bid) { + return BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(singletonList(givenBidderBid(bid))) + .build(), + 1); + } + + private List givenAuctionParticipation( + BidderResponse bidderResponse, BidRequest bidRequest) { + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidRequest(bidRequest) + .build(); + + return List.of(AuctionParticipation.builder() + .bidder("bidder") + .bidderRequest(bidderRequest) + .bidderResponse(bidderResponse) + .build()); + } + + private AuctionContext givenAuctionContext(BidRequest bidRequest) { + return AuctionContext.builder() + .bidRequest(bidRequest) + .bidRejectionTrackers(Map.of("bidder", new BidRejectionTracker("bidder", Set.of(), 1))) + .build(); + } + + private static BidRequest givenBidRequest(List imp, + UnaryOperator bidRequestBuilderCustomizer) { + + return bidRequestBuilderCustomizer + .apply(BidRequest.builder().cur(singletonList("USD")).imp(imp).tmax(500L)) + .build(); + } + + private static Imp givenImp(T ext, Function impBuilderCustomizer) { + return impBuilderCustomizer.apply(Imp.builder() + .id(UUID.randomUUID().toString()) + .ext(mapper.valueToTree(singletonMap( + "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap())))) + .build(); + } + + private static BidderBid givenBidderBid(Bid bid) { + return BidderBid.of(bid, banner, null); + } + + private static BidderBid givenBidderBid(Bid bid, String currency) { + return BidderBid.of(bid, banner, currency); + } + + private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) { + return BidderBid.of(bid, type, currency); + } + + private static Map doubleMap(K key1, V value1, K key2, V value2) { + final Map map = new HashMap<>(); + map.put(key1, value1); + map.put(key2, value2); + return map; + } +} diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 0c7710dbc74..e058d43b0df 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -25,7 +25,6 @@ import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -43,7 +42,6 @@ import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -70,13 +68,11 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.ExecutionAction; @@ -113,7 +109,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; @@ -128,7 +123,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.proto.openrtb.ext.request.TraceLevel; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; @@ -156,8 +150,6 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.Ortb; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.io.IOException; import java.math.BigDecimal; @@ -209,11 +201,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; -import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @ExtendWith(MockitoExtension.class) public class ExchangeServiceTest extends VertxTest { @@ -257,12 +246,6 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private HttpBidderRequester httpBidderRequester; - @Mock(strictness = LENIENT) - private ResponseBidValidator responseBidValidator; - - @Mock(strictness = LENIENT) - private CurrencyConversionService currencyService; - @Mock(strictness = LENIENT) private BidResponseCreator bidResponseCreator; @@ -278,17 +261,11 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private PriceFloorAdjuster priceFloorAdjuster; - @Mock(strictness = LENIENT) - private PriceFloorEnforcer priceFloorEnforcer; - @Mock(strictness = LENIENT) private PriceFloorProcessor priceFloorProcessor; @Mock(strictness = LENIENT) - private DsaEnforcer dsaEnforcer; - - @Mock(strictness = LENIENT) - private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private BidsAdjuster bidsAdjuster; @Mock private Metrics metrics; @@ -374,14 +351,12 @@ public void setUp() { false, AuctionResponsePayloadImpl.of(invocation.getArgument(0))))); + given(bidsAdjuster.validateAndAdjustBids(any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(mediaTypeProcessor.process(any(), anyString(), any(), any())) .willAnswer(invocation -> MediaTypeProcessingResult.succeeded(invocation.getArgument(0), emptyList())); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - given(uidUpdater.updateUid(any(), any(), any())) .willAnswer(inv -> Optional.ofNullable((AuctionContext) inv.getArgument(1)) .map(AuctionContext::getBidRequest) @@ -397,14 +372,11 @@ public void setUp() { given(storedResponseProcessor.updateStoredBidResponse(any())) .willAnswer(inv -> inv.getArgument(0)); - given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); - given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); given(priceFloorAdjuster.adjustForImp(any(), any(), any(), any(), any())) .willAnswer(inv -> Price.of( ((Imp) inv.getArgument(0)).getBidfloorcur(), ((Imp) inv.getArgument(0)).getBidfloor())); - given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null); given(priceFloorProcessor.enrichWithPriceFloors(any(), any(), any(), any(), any())) .willAnswer(inv -> inv.getArgument(0)); @@ -1512,12 +1484,10 @@ public void shouldCallBidResponseCreatorWithExpectedParamsAndUpdateDebugErrors() verify(bidResponseCreator) .create(contextArgumentCaptor.capture(), eq(expectedCacheInfo), eq(expectedMultiBidMap)); - final ObjectNode expectedBidExt = mapper.createObjectNode().put("origbidcpm", new BigDecimal("7.89")); final Bid expectedThirdBid = Bid.builder() .id("bidId3") .impid("impId3") .price(BigDecimal.valueOf(7.89)) - .ext(expectedBidExt) .build(); final List auctionParticipations = contextArgumentCaptor.getValue().getAuctionParticipations(); @@ -1655,81 +1625,6 @@ public void shouldTolerateNullRequestExtPrebidTargeting() { .allSatisfy(map -> assertThat(map).isNull()); } - @Test - public void shouldTolerateResponseBidValidationErrors() { - // given - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .auctiontimestamp(1000L) - .build()))); - - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.error( - singletonList("bid validation warning"), - "bid validation error")); - - givenBidResponseCreator(singletonList(Bid.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .isEmpty(); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly( - BidderError.invalidBid("BidId `bidId1` validation messages: Error: bid validation error." - + " Warning: bid validation warning")); - } - - @Test - public void shouldTolerateResponseBidValidationWarnings() { - // given - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .auctiontimestamp(1000L) - .build()))); - - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success( - singletonList("bid validation warning"))); - - givenBidResponseCreator(singletonList(Bid.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .hasSize(1); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(BidderError.invalidBid( - "BidId `bidId1` validation messages: Warning: bid validation warning")); - } - @Test public void shouldRejectBidIfCurrencyIsNotValid() { // given @@ -1744,9 +1639,6 @@ public void shouldRejectBidIfCurrencyIsNotValid() { .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any(), any(), any(), any())) - .willReturn(ValidationResult.error("BidResponse currency is not valid: USDD")); - final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), "BidResponse currency is not valid: USDD")); givenBidResponseCreator(singletonMap("bidder1", bidderErrors)); @@ -3052,347 +2944,6 @@ public void holdAuctionShouldFailWhenSiteAppAndDoohArePresentInBidRequestAndStri verify(metrics).updateAlertsMetrics(MetricName.general); } - @Test - public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - givenBidResponseCreator(singletonList(Bid.builder().price(updatedPrice).build())); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice).containsExactly(updatedPrice); - } - - @Test - public void shouldApplyStoredBidResponseAdjustments() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(TEN); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(storedResponseProcessor).updateStoredBidResponse(any()); - } - - @Test - public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - // returns the same price as in argument - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice).containsExactly(BigDecimal.ONE); - } - - @Test - public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { - // given - final Bidder bidder = mock(Bidder.class); - final List bids = List.of( - Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), - Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), - Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), - Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); - final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); - - givenBidder("bidder", bidder, seatBid); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - final AuctionContext givenContext = givenRequestContext(bidRequest); - - // when - final AuctionContext result = target.holdAuction(givenContext).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid).hasSize(1); - assertThat(givenContext.getDebugWarnings()) - .containsExactlyInAnyOrder( - "Dropped bid 'invalid_bid_1'. Does not contain a positive (or zero if there is a deal) 'price'", - "Dropped bid 'invalid_bid_2'. Does not contain a positive (or zero if there is a deal) 'price'", - "Dropped bid 'invalid_bid_3'. Does not contain a positive (or zero if there is a deal) 'price'" - ); - verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); - } - - @Test - public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2.0)).build(), "CUR")))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD"); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).isEmpty(); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", TEN); - - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(TEN); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BigDecimal updatedPrice = BigDecimal.valueOf(100); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).isEmpty(); - } - - @Test - public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { - // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); - givenBidder("bidder", mock(Bidder.class), givenSeatBid(asList( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), - givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2")))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) - .willThrow( - new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "CUR1"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1"); - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); - - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { - // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD")))); - givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId2").price(BigDecimal.valueOf(10.0)).build(), "CUR")))); - - final BidRequest bidRequest = BidRequest.builder().cur(singletonList("BAD")) - .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), - identity()))).build(); - - final BigDecimal updatedPrice = BigDecimal.valueOf(20); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD"))) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD")); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD")); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(2); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "USD"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD"); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsOnly(expectedBidderBid); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD"); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCurrency() { - // given - final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); - givenBidder("bidder", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(bidderPrice).build(), "USD")))); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.cur(asList("CUR1", "CUR2", "CUR2"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1")); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BidderError expectedError = BidderError.badInput("Cur parameter contains more than one currency." - + " CUR1 will be used"); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { - // given - final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); - final BigDecimal bidder2Price = BigDecimal.valueOf(2); - final BigDecimal bidder3Price = BigDecimal.valueOf(3); - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR")))); - givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP")))); - givenBidder("bidder3", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD")))); - - final Map impBidders = new HashMap<>(); - impBidders.put("bidder1", 1); - impBidders.put("bidder2", 2); - impBidders.put("bidder3", 3); - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(impBidders, identity())), builder -> builder.cur(singletonList("USD"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); - verifyNoMoreInteractions(currencyService); - - assertThat(contextArgumentCaptor.getValue().getAuctionParticipations()) - .hasSize(3) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsOnly(bidder3Price, updatedPrice, updatedPrice); - } - @Test public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsService() { // given @@ -3541,351 +3092,6 @@ public void shouldPassResponseToPostProcessor() { .build())); } - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(2.468)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(4.936)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(1).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - BidderBid.of(Bid.builder().impid("1234").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build()), - BidderBid.builder().type(xNative).bid(givenBid(identity())).build(), - BidderBid.builder().type(audio).bid(givenBid(identity())).build()))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - } - - @Test - public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { - // given - final Bidder bidder = mock(Bidder.class); - - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .auctiontimestamp(1000L) - .currency(ExtRequestCurrency.of(null, false)) - .bidadjustmentfactors(givenAdjustments) - .build()))); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.ONE); - } - - @Test - public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { - // given - final BidderBid bidToAccept = - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(ONE).build(), "USD"); - final BidderBid bidToReject = - givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(TEN).build(), "USD"); - - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(List.of(bidToAccept, bidToReject))); - - final BidRequest bidRequest = givenBidRequest(List.of( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), - identity()); - - given(priceFloorEnforcer.enforce(any(), any(), any(), any())) - .willReturn(AuctionParticipation.builder() - .bidder("bidder1") - .bidderResponse(BidderResponse.of( - "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) - .build()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsExactly(bidToAccept); - } - - @Test - public void shouldReturnBidsAcceptedByDsaEnforcer() { - // given - final BidderBid bidToAccept = - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(ONE).build(), "USD"); - final BidderBid bidToReject = - givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(TEN).build(), "USD"); - - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(List.of(bidToAccept, bidToReject))); - - final BidRequest bidRequest = givenBidRequest(List.of( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), - identity()); - - given(dsaEnforcer.enforce(any(), any(), any())) - .willReturn(AuctionParticipation.builder() - .bidder("bidder1") - .bidderResponse(BidderResponse.of( - "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) - .build()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsExactly(bidToAccept); - } - @Test public void shouldReturnBidResponseModifiedByAuctionResponseHooks() { // given @@ -4590,9 +3796,6 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb givenBidder(givenSingleSeatBid(bidderBid)); - given(responseBidValidator.validate(any(), any(), any(), any())) - .willReturn(ValidationResult.success()); - // when target.holdAuction(auctionContext); @@ -4786,6 +3989,38 @@ public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() { assertThat(timeoutCaptor.getAllValues()).containsExactly(450L); } + @Test + public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { + // given + final Bidder bidder = mock(Bidder.class); + final List bids = List.of( + Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), + Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), + Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), + Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); + final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); + + givenBidder("bidder", bidder, seatBid); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + final AuctionContext givenContext = givenRequestContext(bidRequest); + + // when + final AuctionContext result = target.holdAuction(givenContext).result(); + + // then + assertThat(result.getBidResponse().getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1); + assertThat(givenContext.getDebugWarnings()) + .containsExactlyInAnyOrder( + "Dropped bid 'invalid_bid_1'. Does not contain a positive (or zero if there is a deal) 'price'", + "Dropped bid 'invalid_bid_2'. Does not contain a positive (or zero if there is a deal) 'price'", + "Dropped bid 'invalid_bid_3'. Does not contain a positive (or zero if there is a deal) 'price'" + ); + verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); + } + private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { target = new ExchangeService( 0, @@ -4801,17 +4036,13 @@ private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { timeoutFactory, ortbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, priceFloorProcessor, - dsaEnforcer, - bidAdjustmentFactorResolver, + bidsAdjuster, metrics, clock, jacksonMapper, From 4e21d57a856a716fe3d5d27b4db86ce600a7debb Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:24:36 +0300 Subject: [PATCH 078/170] Tests: Update functional test for Ortb2Blocking module (#3472) --- .../ortb2blocking/Ortb2BlockingSpec.groovy | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy index 5b9d96e6bf3..4df794b3ead 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -651,16 +651,15 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert !response?.ext?.prebid?.modules?.warnings } - @PendingFeature def "PBS should be able to override enforcement by deal id"() { given: "Default bidRequest" def bidRequest = BidRequest.defaultBidRequest and: "Account in the DB with blocking configuration" - def blockingCondition = new Ortb2BlockingConditions(dealIds: [dealId.toString()]) - def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { + def blockingCondition = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: new Ortb2BlockingConditions(dealIds: [dealId.toString()])) + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2AttributesForDeals]).tap { enforceBlocks = true - actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, null, [blockingCondition]) } def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) accountDao.save(account) @@ -686,15 +685,15 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert !response?.ext?.prebid?.modules?.warnings where: - dealId | ortb2Attributes | attributeName - PBSUtils.randomNumber | PBSUtils.randomString | BADV - PBSUtils.randomNumber | PBSUtils.randomString | BAPP - PBSUtils.randomNumber | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR - WILDCARD | PBSUtils.randomString | BADV - WILDCARD | PBSUtils.randomString | BAPP - WILDCARD | PBSUtils.randomString | BCAT - WILDCARD | PBSUtils.randomNumber | BATTR + dealId | ortb2Attributes | ortb2AttributesForDeals | attributeName + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BADV + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BAPP + WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BCAT + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR } def "PBS should be able to override blocked ortb2 attribute by bidder"() { From b0a953f08f9e9bc81b79171dd5c34fa8423f7915 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:40:35 +0200 Subject: [PATCH 079/170] Core: Rename Response Correction Hook (#3468) --- .../v1/ResponseCorrectionAllProcessedBidResponsesHook.java | 2 +- .../functional/model/config/ModuleHookImplementation.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java index cea7a80b131..09e4640b064 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -24,7 +24,7 @@ public class ResponseCorrectionAllProcessedBidResponsesHook implements AllProcessedBidResponsesHook { - private static final String CODE = "pb-response-correction-all-processed-bid-responses-hook"; + private static final String CODE = "pb-response-correction-all-processed-bid-responses"; private final ResponseCorrectionProvider responseCorrectionProvider; private final ObjectMapper mapper; diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index 0d8333b3375..b5c57122a3f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -7,7 +7,7 @@ import org.prebid.server.functional.model.ModuleName enum ModuleHookImplementation { PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"), - RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses-hook"), + RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses"), ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") From a813112b8810ae6cf5b76747e9f5612440526631 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:44:10 +0200 Subject: [PATCH 080/170] Streamlyn: New Adapter (#3473) --- .../bidder-config/limelightDigital.yaml | 3 ++ .../org/prebid/server/it/StreamlynTest.java | 35 ++++++++++++++++ .../test-auction-streamlyn-request.json | 24 +++++++++++ .../test-auction-streamlyn-response.json | 33 +++++++++++++++ .../streamlyn/test-streamlyn-bid-request.json | 40 +++++++++++++++++++ .../test-streamlyn-bid-response.json | 15 +++++++ .../server/it/test-application.properties | 2 + 7 files changed, 152 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/StreamlynTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-response.json diff --git a/src/main/resources/bidder-config/limelightDigital.yaml b/src/main/resources/bidder-config/limelightDigital.yaml index 16d495c8d55..48c238c3840 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -38,6 +38,9 @@ adapters: endpoint: http://ads-pbs.bidder-embi.media/openrtb/{{PublisherID}}?host={{Host}} tgm: enabled: false + streamlyn: + enabled: false + endpoint: http://rtba.bidsxchange.com/openrtb/{{PublisherID}}?host={{Host}} meta-info: maintainer-email: engineering@project-limelight.com app-media-types: diff --git a/src/test/java/org/prebid/server/it/StreamlynTest.java b/src/test/java/org/prebid/server/it/StreamlynTest.java new file mode 100644 index 00000000000..b482c4d2192 --- /dev/null +++ b/src/test/java/org/prebid/server/it/StreamlynTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class StreamlynTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheStreamlynBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/streamlyn-exchange/test.host/123456")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/streamlyn/test-streamlyn-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/streamlyn/test-streamlyn-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/streamlyn/test-auction-streamlyn-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/streamlyn/test-auction-streamlyn-response.json", response, + singletonList("streamlyn")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-request.json b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-request.json new file mode 100644 index 00000000000..d25c7b7bb0e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "streamlyn": { + "host": "test.host", + "publisherId": "123456" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json new file mode 100644 index 00000000000..bd20be683c2 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json @@ -0,0 +1,33 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId", + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "streamlyn", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "streamlyn": "{{ streamlyn.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-request.json new file mode 100644 index 00000000000..8e58e53ba4b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-request.json @@ -0,0 +1,40 @@ +{ + "id": "request_id-imp_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-response.json new file mode 100644 index 00000000000..04d26e04318 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-streamlyn-bid-response.json @@ -0,0 +1,15 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "crid": "creativeId" + } + ] + } + ] +} 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 853cf7c4652..345445cc18e 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -279,6 +279,8 @@ adapters.limelightDigital.aliases.filmzie.enabled=true adapters.limelightDigital.aliases.filmzie.endpoint=http://localhost:8090/filmzie-exchange/{{Host}}/{{PublisherID}} adapters.limelightDigital.aliases.tgm.enabled=true adapters.limelightDigital.aliases.tgm.endpoint=http://localhost:8090/tgm-exchange/{{Host}}/{{PublisherID}} +adapters.limelightDigital.aliases.streamlyn.enabled=true +adapters.limelightDigital.aliases.streamlyn.endpoint=http://localhost:8090/streamlyn-exchange/{{Host}}/{{PublisherID}} adapters.lmkiviads.enabled=true adapters.lmkiviads.endpoint=http://localhost:8090/lm-kiviads-exchange/{{SourceId}}/{{Host}} adapters.lockerdome.enabled=true From 91bec69884a5be797a69f2fd1bf2983f0437e9e3 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:49:49 +0200 Subject: [PATCH 081/170] Core: Passthrough bid.ext.prebid.meta (#3479) --- .../server/bidder/rubicon/RubiconBidderTest.java | 8 ++++++-- .../generic/test-auction-generic-response.json | 6 +++++- .../it/openrtb2/generic/test-generic-bid-response.json | 10 +++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index 47a0694ef93..24db88e46fc 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -3725,13 +3725,17 @@ public void makeBidsShouldReturnBidWithCurrencyFromBidResponse() throws JsonProc } @Test - public void makeBidsShouldReturnBidWithDchainFromRequest() throws JsonProcessingException { + public void makeBidsShouldReturnBidMetaFromRequest() throws JsonProcessingException { // given final ObjectNode requestNode = mapper.valueToTree(ExtBidPrebid.builder() .meta(ExtBidPrebidMeta.builder() .dchain(mapper.createObjectNode().set("dChain", TextNode.valueOf("dChain"))) + .mediaType("banner") .build()) .build()); + + final ObjectNode expectedNode = requestNode.deepCopy(); + final BidderCall httpCall = givenHttpCall( givenBidRequest(identity()), mapper.writeValueAsString(RubiconBidResponse.builder() @@ -3748,7 +3752,7 @@ public void makeBidsShouldReturnBidWithDchainFromRequest() throws JsonProcessing assertThat(result.getValue()) .extracting(BidderBid::getBid) .extracting(Bid::getExt) - .containsExactly(requestNode); + .containsExactly(expectedNode); } @Test diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json index 28cf6da40f7..808b06e512e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json @@ -11,7 +11,11 @@ "ext": { "origbidcpm": 3.33, "prebid": { - "type": "banner" + "type": "banner", + "meta": { + "mediaType": "banner", + "adaptercode": "adaptercode" + } } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json index 04d26e04318..7dc412239c2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-generic-bid-response.json @@ -7,7 +7,15 @@ "id": "bid_id", "impid": "imp_id", "price": 3.33, - "crid": "creativeId" + "crid": "creativeId", + "ext": { + "prebid": { + "meta": { + "mediaType": "banner", + "adaptercode": "adaptercode" + } + } + } } ] } From 31d0738a526027afd43e48a9a162ea3d24e8a98f Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:57:18 +0200 Subject: [PATCH 082/170] Core: Update Currency Warning (#3459) --- .../prebid/server/auction/BidsAdjuster.java | 9 ++-- .../functional/tests/CurrencySpec.groovy | 44 +++++++++++++++++++ .../server/auction/BidsAdjusterTest.java | 10 +++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java index ec6b1c9d2f3..8134662fcd9 100644 --- a/src/main/java/org/prebid/server/auction/BidsAdjuster.java +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -95,8 +95,9 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar final List requestCurrencies = bidRequest.getCur(); if (requestCurrencies.size() > 1) { - errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" - .formatted(requestCurrencies.getFirst()))); + warnings.add(BidderError.badInput( + "a single currency (" + requestCurrencies.getFirst() + ") has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency.")); } final List bids = seatBid.getBids(); @@ -118,9 +119,7 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar } } - final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() - ? bidderResponse - : bidderResponse.with( + final BidderResponse resultBidderResponse = bidderResponse.with( seatBid.toBuilder() .bids(validBids) .errors(errors) diff --git a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy index 568f202be55..df5bba70028 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -14,6 +14,7 @@ import static org.prebid.server.functional.model.Currency.CHF import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer class CurrencySpec extends BaseSpec { @@ -146,6 +147,49 @@ class CurrencySpec extends BaseSpec { CHF || EUR } + def "PBS should emit warning when request contain more that one currency"() { + given: "Default BidRequest with currencies" + def currencies = [EUR, USD] + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = currencies + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain first requested currency" + assert bidResponse.cur == currencies[0] + + and: "Bidder request should contain requested currencies" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == currencies + + and: "Bid response should contain warnings" + assert bidResponse.ext.warnings[GENERIC]?.message == ["a single currency (${currencies[0]}) has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency." as String] + } + + def "PBS shouldn't emit warning when request contain one currency"() { + given: "Default BidRequest with currency" + def currency = [USD] + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = currency + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain first requested currency" + assert bidResponse.cur == currency[0] + + and: "Bidder request should contain requested currency" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == currency + + and: "Bid response shouldn't contain warnings" + assert !bidResponse.ext.warnings + } + private static Map getExternalCurrencyConverterConfig() { ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, "currency-converter.external-rates.enabled" : "true", diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java index 2691b629544..8b7dd0589b0 100644 --- a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -332,7 +332,7 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported } @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCurrency() { + public void shouldUpdateBidPriceWithCurrencyConversionAndAddWarningAboutMultipleCurrency() { // given final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); final BidderResponse bidderResponse = BidderResponse.of( @@ -363,14 +363,16 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCu assertThat(result).hasSize(1); - final BidderError expectedError = BidderError.badInput("Cur parameter contains more than one currency." - + " CUR1 will be used"); final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); assertThat(firstSeatBid.getBids()) .extracting(BidderBid::getBid) .flatExtracting(Bid::getPrice) .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + + final BidderError expectedWarning = BidderError.badInput( + "a single currency (CUR1) has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency."); + assertThat(firstSeatBid.getWarnings()).containsOnly(expectedWarning); } @Test From 075bdf81e028398bbdcdcb460214ed87b677ed84 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:52:54 +0200 Subject: [PATCH 083/170] Core: Add storedAuctionResponse on imp level. (#3461) --- .../server/auction/BidResponseCreator.java | 6 +- .../auction/StoredResponseProcessor.java | 388 ++++++++++-------- .../ext/request/ExtStoredAuctionResponse.java | 3 + .../server/validation/ImpValidator.java | 5 +- .../auction/StoredAuctionResponse.groovy | 2 + .../tests/StoredResponseSpec.groovy | 126 ++++++ .../auction/SkippedAuctionServiceTest.java | 15 +- .../auction/StoredResponseProcessorTest.java | 80 +++- .../server/validation/ImpValidatorTest.java | 34 +- 9 files changed, 459 insertions(+), 200 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 567f73e63de..1c0b837bb4e 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -18,6 +18,7 @@ import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -191,10 +192,11 @@ Future createOnSkippedAuction(AuctionContext auctionContext, List cur = bidRequest.getCur(); final BidResponse bidResponse = BidResponse.builder() .id(bidRequest.getId()) - .cur(Stream.ofNullable(bidRequest.getCur()).flatMap(Collection::stream).findFirst().orElse(null)) - .seatbid(Optional.ofNullable(seatBids).orElse(Collections.emptyList())) + .cur(CollectionUtils.isNotEmpty(cur) ? cur.getFirst() : null) + .seatbid(ListUtils.emptyIfNull(seatBids)) .ext(extBidResponse) .build(); diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java index 64ec4869a9b..b769d2974b1 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java @@ -69,31 +69,30 @@ public StoredResponseProcessor(ApplicationSettings applicationSettings, Future getStoredResponseResult(List imps, Timeout timeout) { final Map impExtPrebids = getImpsExtPrebid(imps); - final Map auctionStoredResponseToImpId = getAuctionStoredResponses(impExtPrebids); - final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, auctionStoredResponseToImpId); + final Map impIdsToStoredResponses = getAuctionStoredResponses(impExtPrebids); + final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, impIdsToStoredResponses); - final Map> impToBidderToStoredBidResponseId = getStoredBidResponses(impExtPrebids, - requiredRequestImps); + final Map> impToBidderToStoredBidResponseId = + getStoredBidResponses(impExtPrebids, requiredRequestImps); - final Set storedIds = new HashSet<>(auctionStoredResponseToImpId.keySet()); + final Set storedResponses = new HashSet<>(impIdsToStoredResponses.values()); - storedIds.addAll( - impToBidderToStoredBidResponseId.values().stream() - .flatMap(bidderToId -> bidderToId.values().stream()) - .collect(Collectors.toSet())); + impToBidderToStoredBidResponseId.values() + .forEach(bidderToStoredResponse -> storedResponses.addAll(bidderToStoredResponse.values())); - if (storedIds.isEmpty()) { - return Future.succeededFuture(StoredResponseResult.of(imps, Collections.emptyList(), - Collections.emptyMap())); + if (storedResponses.isEmpty()) { + return Future.succeededFuture( + StoredResponseResult.of(imps, Collections.emptyList(), Collections.emptyMap())); } - return applicationSettings.getStoredResponses(storedIds, timeout) + return getStoredResponses(storedResponses, timeout) .recover(exception -> Future.failedFuture(new InvalidRequestException( "Stored response fetching failed with reason: " + exception.getMessage()))) .map(storedResponseDataResult -> StoredResponseResult.of( requiredRequestImps, - convertToSeatBid(storedResponseDataResult, auctionStoredResponseToImpId), - mapStoredBidResponseIdsToValues(storedResponseDataResult.getIdToStoredResponses(), + convertToSeatBid(storedResponseDataResult, impIdsToStoredResponses), + mapStoredBidResponseIdsToValues( + storedResponseDataResult.getIdToStoredResponses(), impToBidderToStoredBidResponseId))); } @@ -107,161 +106,95 @@ Future getStoredResponseResult(String storedId, Timeout ti Collections.emptyMap())); } - private List excludeStoredAuctionResponseImps(List imps, - Map auctionStoredResponseToImpId) { - + private Map getImpsExtPrebid(List imps) { return imps.stream() - .filter(imp -> !auctionStoredResponseToImpId.containsValue(imp.getId())) - .toList(); - } - - public List updateStoredBidResponse(List auctionParticipations) { - return auctionParticipations.stream() - .map(StoredResponseProcessor::updateStoredBidResponse) - .collect(Collectors.toList()); + .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); } - private static AuctionParticipation updateStoredBidResponse(AuctionParticipation auctionParticipation) { - final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); - final BidRequest bidRequest = bidderRequest.getBidRequest(); - - final List imps = bidRequest.getImp(); - // Аor now, Stored Bid Response works only for bid requests with single imp - if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) { - return auctionParticipation; + private ExtImp getExtImp(ObjectNode extImpNode, String impId) { + try { + return mapper.mapper().treeToValue(extImpNode, ExtImp.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException( + "Error decoding bidRequest.imp.ext for impId = %s : %s".formatted(impId, e.getMessage())); } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid initialSeatBid = bidderResponse.getSeatBid(); - final BidderSeatBid adjustedSeatBid = updateSeatBid(initialSeatBid, imps.getFirst().getId()); - - return auctionParticipation.with(bidderResponse.with(adjustedSeatBid)); - } - - private static BidderSeatBid updateSeatBid(BidderSeatBid bidderSeatBid, String impId) { - final List bids = bidderSeatBid.getBids().stream() - .map(bidderBid -> resolveBidImpId(bidderBid, impId)) - .collect(Collectors.toList()); - - return bidderSeatBid.with(bids); } - private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { - final Bid bid = bidderBid.getBid(); - final String bidImpId = bid.getImpid(); - if (!StringUtils.contains(bidImpId, PBS_IMPID_MACRO)) { - return bidderBid; - } - - return bidderBid.toBuilder() - .bid(bid.toBuilder().impid(bidImpId.replace(PBS_IMPID_MACRO, impId)).build()) - .build(); + private Map getAuctionStoredResponses(Map extImpPrebids) { + return extImpPrebids.entrySet().stream() + .map(impIdToExtPrebid -> Tuple2.of( + impIdToExtPrebid.getKey(), + extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) + .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) + .collect(Collectors.toMap(Tuple2::getLeft, Tuple2::getRight)); } - List mergeWithBidderResponses(List auctionParticipations, - List storedAuctionResponses, - List imps, - Map bidRejectionTrackers) { - if (CollectionUtils.isEmpty(storedAuctionResponses)) { - return auctionParticipations; - } - - final Map bidderToAuctionParticipation = auctionParticipations.stream() - .collect(Collectors.toMap(AuctionParticipation::getBidder, Function.identity())); - final Map bidderToSeatBid = storedAuctionResponses.stream() - .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); - final Map impIdToBidType = imps.stream() - .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); - final Set responseBidders = new HashSet<>(bidderToAuctionParticipation.keySet()); - responseBidders.addAll(bidderToSeatBid.keySet()); - - return responseBidders.stream() - .map(bidder -> updateBidderResponse(bidderToAuctionParticipation.get(bidder), - bidderToSeatBid.get(bidder), impIdToBidType)) - .map(auctionParticipation -> restoreStoredBidsFromRejection(bidRejectionTrackers, auctionParticipation)) - .toList(); + private StoredResponse extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { + final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); + return Optional.ofNullable(storedAuctionResponse) + .map(ExtStoredAuctionResponse::getSeatBid) + .map(StoredResponse.StoredResponseObject::new) + .or(() -> Optional.ofNullable(storedAuctionResponse) + .map(ExtStoredAuctionResponse::getId) + .map(StoredResponse.StoredResponseId::new)) + .orElse(null); } - private static AuctionParticipation restoreStoredBidsFromRejection( - Map bidRejectionTrackers, - AuctionParticipation auctionParticipation) { - - final BidRejectionTracker bidRejectionTracker = bidRejectionTrackers.get(auctionParticipation.getBidder()); - - if (bidRejectionTracker != null) { - Optional.ofNullable(auctionParticipation.getBidderResponse()) - .map(BidderResponse::getSeatBid) - .map(BidderSeatBid::getBids) - .ifPresent(bidRejectionTracker::restoreFromRejection); - } - - return auctionParticipation; - } + private List excludeStoredAuctionResponseImps(List imps, + Map impIdToStoredResponse) { - private Map getImpsExtPrebid(List imps) { return imps.stream() - .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); - } - - private Map getAuctionStoredResponses(Map extImpPrebids) { - return extImpPrebids.entrySet().stream() - .map(impIdToExtPrebid -> Tuple2.of(impIdToExtPrebid.getKey(), - extractAuctionStoredResponseId(impIdToExtPrebid.getValue()))) - .filter(impIdToStoredResponseId -> impIdToStoredResponseId.getRight() != null) - .collect(Collectors.toMap(Tuple2::getRight, Tuple2::getLeft)); + .filter(imp -> !impIdToStoredResponse.containsKey(imp.getId())) + .toList(); } - private String extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { - final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); - return storedAuctionResponse != null ? storedAuctionResponse.getId() : null; - } + private Map> getStoredBidResponses( + Map extImpPrebids, + List imps) { - private Map> getStoredBidResponses(Map extImpPrebids, - List imps) { // PBS supports stored bid response only for requests with single impression, but it can be changed in future if (imps.size() != 1) { return Collections.emptyMap(); } - final Set impsIds = imps.stream().map(Imp::getId).collect(Collectors.toSet()); - return extImpPrebids.entrySet().stream() - .filter(impIdToExtPrebid -> impsIds.contains(impIdToExtPrebid.getKey())) - .filter(impIdToExtPrebid -> CollectionUtils - .isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) - .collect(Collectors.toMap(Map.Entry::getKey, + .filter(impIdToExtPrebid -> + CollectionUtils.isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) + .collect(Collectors.toMap( + Map.Entry::getKey, impIdToStoredResponses -> resolveStoredBidResponse(impIdToStoredResponses.getValue().getStoredBidResponse()))); } - private ExtImp getExtImp(ObjectNode extImpNode, String impId) { - try { - return mapper.mapper().treeToValue(extImpNode, ExtImp.class); - } catch (JsonProcessingException e) { - throw new InvalidRequestException( - "Error decoding bidRequest.imp.ext for impId = %s : %s".formatted(impId, e.getMessage())); - } - } + private Map resolveStoredBidResponse( + List storedBidResponse) { - private Map resolveStoredBidResponse(List storedBidResponse) { return storedBidResponse.stream() - .collect(Collectors.toMap(ExtStoredBidResponse::getBidder, ExtStoredBidResponse::getId)); + .collect(Collectors.toMap( + ExtStoredBidResponse::getBidder, + extStoredBidResponse -> new StoredResponse.StoredResponseId(extStoredBidResponse.getId()))); + } + + private Future getStoredResponses(Set storedResponses, Timeout timeout) { + return applicationSettings.getStoredResponses( + storedResponses.stream() + .filter(StoredResponse.StoredResponseId.class::isInstance) + .map(StoredResponse.StoredResponseId.class::cast) + .map(StoredResponse.StoredResponseId::id) + .collect(Collectors.toSet()), + timeout); } private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult, - Map auctionStoredResponses) { + Map impIdsToStoredResponses) { + final List resolvedSeatBids = new ArrayList<>(); final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); - for (final Map.Entry storedIdToImpId : auctionStoredResponses.entrySet()) { - final String id = storedIdToImpId.getKey(); - final String impId = storedIdToImpId.getValue(); - final String rowSeatBid = idToStoredResponses.get(id); - if (rowSeatBid == null) { - throw new InvalidRequestException( - "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." - .formatted(impId, id)); - } - final List seatBids = parseSeatBid(id, rowSeatBid); + for (Map.Entry impIdToStoredResponse : impIdsToStoredResponses.entrySet()) { + final String impId = impIdToStoredResponse.getKey(); + final StoredResponse storedResponse = impIdToStoredResponse.getValue(); + final List seatBids = resolveSeatBids(storedResponse, idToStoredResponses, impId); + validateStoredSeatBid(seatBids); resolvedSeatBids.addAll(seatBids.stream() .map(seatBid -> updateSeatBidBids(seatBid, impId)) @@ -273,7 +206,7 @@ private List convertToSeatBid(StoredResponseDataResult storedResponseDa private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult) { final List resolvedSeatBids = new ArrayList<>(); final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); - for (final Map.Entry storedIdToImpId : idToStoredResponses.entrySet()) { + for (Map.Entry storedIdToImpId : idToStoredResponses.entrySet()) { final String id = storedIdToImpId.getKey(); final String rowSeatBid = storedIdToImpId.getValue(); if (rowSeatBid == null) { @@ -287,6 +220,25 @@ private List convertToSeatBid(StoredResponseDataResult storedResponseDa return mergeSameBidderSeatBid(resolvedSeatBids); } + private List resolveSeatBids(StoredResponse storedResponse, + Map idToStoredResponses, + String impId) { + + if (storedResponse instanceof StoredResponse.StoredResponseObject storedResponseObject) { + return Collections.singletonList(storedResponseObject.seatBid()); + } + + final String storedResponseId = ((StoredResponse.StoredResponseId) storedResponse).id(); + final String rowSeatBid = idToStoredResponses.get(storedResponseId); + if (rowSeatBid == null) { + throw new InvalidRequestException( + "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." + .formatted(impId, storedResponseId)); + } + + return parseSeatBid(storedResponseId, rowSeatBid); + } + private List parseSeatBid(String id, String rowSeatBid) { try { return mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPE); @@ -295,18 +247,6 @@ private List parseSeatBid(String id, String rowSeatBid) { } } - private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { - return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); - } - - private List updateBidsWithImpId(List bids, String impId) { - return bids.stream().map(bid -> updateBidWithImpId(bid, impId)).toList(); - } - - private static Bid updateBidWithImpId(Bid bid, String impId) { - return bid.toBuilder().impid(impId).build(); - } - private void validateStoredSeatBid(List seatBids) { for (final SeatBid seatBid : seatBids) { if (StringUtils.isEmpty(seatBid.getSeat())) { @@ -319,6 +259,18 @@ private void validateStoredSeatBid(List seatBids) { } } + private SeatBid updateSeatBidBids(SeatBid seatBid, String impId) { + return seatBid.toBuilder().bid(updateBidsWithImpId(seatBid.getBid(), impId)).build(); + } + + private List updateBidsWithImpId(List bids, String impId) { + return bids.stream().map(bid -> updateBidWithImpId(bid, impId)).toList(); + } + + private static Bid updateBidWithImpId(Bid bid, String impId) { + return bid.toBuilder().impid(impId).build(); + } + private List mergeSameBidderSeatBid(List seatBids) { return seatBids.stream().collect(Collectors.groupingBy(SeatBid::getSeat, Collectors.toList())) .entrySet().stream() @@ -336,23 +288,108 @@ private SeatBid makeMergedSeatBid(String seat, List storedSeatBids) { private Map> mapStoredBidResponseIdsToValues( Map idToStoredResponses, - Map> impToBidderToStoredBidResponseId) { + Map> impToBidderToStoredBidResponseId) { return impToBidderToStoredBidResponseId.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, entry -> entry.getValue().entrySet().stream() - .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue())) + .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue().id())) .collect(Collectors.toMap( Map.Entry::getKey, - bidderToId -> idToStoredResponses.get(bidderToId.getValue()), + bidderToId -> idToStoredResponses.get(bidderToId.getValue().id()), (first, second) -> second, CaseInsensitiveMap::new)))); } + public List updateStoredBidResponse(List auctionParticipations) { + return auctionParticipations.stream() + .map(StoredResponseProcessor::updateStoredBidResponse) + .collect(Collectors.toList()); + } + + private static AuctionParticipation updateStoredBidResponse(AuctionParticipation auctionParticipation) { + final BidderRequest bidderRequest = auctionParticipation.getBidderRequest(); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + + final List imps = bidRequest.getImp(); + // Аor now, Stored Bid Response works only for bid requests with single imp + if (imps.size() > 1 || StringUtils.isEmpty(bidderRequest.getStoredResponse())) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid initialSeatBid = bidderResponse.getSeatBid(); + final BidderSeatBid adjustedSeatBid = updateSeatBid(initialSeatBid, imps.getFirst().getId()); + + return auctionParticipation.with(bidderResponse.with(adjustedSeatBid)); + } + + private static BidderSeatBid updateSeatBid(BidderSeatBid bidderSeatBid, String impId) { + final List bids = bidderSeatBid.getBids().stream() + .map(bidderBid -> resolveBidImpId(bidderBid, impId)) + .collect(Collectors.toList()); + + return bidderSeatBid.with(bids); + } + + private static BidderBid resolveBidImpId(BidderBid bidderBid, String impId) { + final Bid bid = bidderBid.getBid(); + final String bidImpId = bid.getImpid(); + if (!StringUtils.contains(bidImpId, PBS_IMPID_MACRO)) { + return bidderBid; + } + + return bidderBid.toBuilder() + .bid(bid.toBuilder().impid(bidImpId.replace(PBS_IMPID_MACRO, impId)).build()) + .build(); + } + + List mergeWithBidderResponses(List auctionParticipations, + List storedAuctionResponses, + List imps, + Map bidRejectionTrackers) { + + if (CollectionUtils.isEmpty(storedAuctionResponses)) { + return auctionParticipations; + } + + final Map bidderToAuctionParticipation = auctionParticipations.stream() + .collect(Collectors.toMap(AuctionParticipation::getBidder, Function.identity())); + final Map bidderToSeatBid = storedAuctionResponses.stream() + .collect(Collectors.toMap(SeatBid::getSeat, Function.identity())); + final Map impIdToBidType = imps.stream() + .collect(Collectors.toMap(Imp::getId, this::resolveBidType)); + final Set responseBidders = new HashSet<>(bidderToAuctionParticipation.keySet()); + responseBidders.addAll(bidderToSeatBid.keySet()); + + return responseBidders.stream() + .map(bidder -> updateBidderResponse( + bidderToAuctionParticipation.get(bidder), + bidderToSeatBid.get(bidder), + impIdToBidType)) + .map(auctionParticipation -> restoreStoredBidsFromRejection(bidRejectionTrackers, auctionParticipation)) + .toList(); + } + + private BidType resolveBidType(Imp imp) { + BidType bidType = BidType.banner; + if (imp.getBanner() != null) { + return bidType; + } else if (imp.getVideo() != null) { + bidType = BidType.video; + } else if (imp.getXNative() != null) { + bidType = BidType.xNative; + } else if (imp.getAudio() != null) { + bidType = BidType.audio; + } + return bidType; + } + private AuctionParticipation updateBidderResponse(AuctionParticipation auctionParticipation, SeatBid storedSeatBid, Map impIdToBidType) { + if (auctionParticipation != null) { if (auctionParticipation.isRequestBlocked()) { return auctionParticipation; @@ -377,13 +414,17 @@ private AuctionParticipation updateBidderResponse(AuctionParticipation auctionPa } } - private BidderSeatBid makeBidderSeatBid(BidderSeatBid bidderSeatBid, SeatBid seatBid, + private BidderSeatBid makeBidderSeatBid(BidderSeatBid bidderSeatBid, + SeatBid seatBid, Map impIdToBidType) { + final boolean nonNullBidderSeatBid = bidderSeatBid != null; final String bidCurrency = nonNullBidderSeatBid ? bidderSeatBid.getBids().stream() - .map(BidderBid::getBidCurrency).filter(Objects::nonNull) - .findAny().orElse(DEFAULT_BID_CURRENCY) + .map(BidderBid::getBidCurrency) + .filter(Objects::nonNull) + .findAny() + .orElse(DEFAULT_BID_CURRENCY) : DEFAULT_BID_CURRENCY; final List bidderBids = seatBid != null ? seatBid.getBid().stream() @@ -416,17 +457,28 @@ private ExtBidPrebid parseExtBidPrebid(ObjectNode bidExtPrebid) { } } - private BidType resolveBidType(Imp imp) { - BidType bidType = BidType.banner; - if (imp.getBanner() != null) { - return bidType; - } else if (imp.getVideo() != null) { - bidType = BidType.video; - } else if (imp.getXNative() != null) { - bidType = BidType.xNative; - } else if (imp.getAudio() != null) { - bidType = BidType.audio; + private static AuctionParticipation restoreStoredBidsFromRejection( + Map bidRejectionTrackers, + AuctionParticipation auctionParticipation) { + + final BidRejectionTracker bidRejectionTracker = bidRejectionTrackers.get(auctionParticipation.getBidder()); + + if (bidRejectionTracker != null) { + Optional.ofNullable(auctionParticipation.getBidderResponse()) + .map(BidderResponse::getSeatBid) + .map(BidderSeatBid::getBids) + .ifPresent(bidRejectionTracker::restoreFromRejection); + } + + return auctionParticipation; + } + + private sealed interface StoredResponse { + + record StoredResponseId(String id) implements StoredResponse { + } + + record StoredResponseObject(SeatBid seatBid) implements StoredResponse { } - return bidType; } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java index 852a106a7ef..e8f83afcccb 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredAuctionResponse.java @@ -15,4 +15,7 @@ public class ExtStoredAuctionResponse { @JsonProperty("seatbidarr") List seatBids; + + @JsonProperty("seatbidobj") + SeatBid seatBid; } diff --git a/src/main/java/org/prebid/server/validation/ImpValidator.java b/src/main/java/org/prebid/server/validation/ImpValidator.java index f90df916af1..d5d9ecba2f1 100644 --- a/src/main/java/org/prebid/server/validation/ImpValidator.java +++ b/src/main/java/org/prebid/server/validation/ImpValidator.java @@ -457,8 +457,9 @@ private void validateImpExtPrebidStoredResponses(ExtImpPrebid extPrebid, + " is not supported at the imp level"); } - if (extStoredAuctionResponse.getId() == null) { - throw new ValidationException("request.imp[%d].ext.prebid.storedauctionresponse.id should be defined", + if (extStoredAuctionResponse.getId() == null && extStoredAuctionResponse.getSeatBid() == null) { + throw new ValidationException( + "request.imp[%d].ext.prebid.storedauctionresponse.{id or seatbidobj} should be defined", impIndex); } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy index 2cab8a9fdc1..cde5f2268de 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/StoredAuctionResponse.groovy @@ -10,4 +10,6 @@ class StoredAuctionResponse { String id @JsonProperty("seatbidarr") List seatBids + @JsonProperty("seatbidobj") + SeatBid seatBidObject } diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy index e2a8a39ffef..fccb14c8bab 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy @@ -2,11 +2,14 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature @@ -249,4 +252,127 @@ class StoredResponseSpec extends BaseSpec { and: "PBS not send request to bidder" assert bidder.getRequestCount(bidRequest.id) == 0 } + + def "PBS should set seatBid in response from single imp.ext.prebid.storedBidResponse.seatbidobj when it is defined"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: storedAuctionResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty seatbid"() { + given: "Default basic BidRequest with empty stored response" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid()) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: Seat can\'t be empty in stored response seatBid' + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty bids"() { + given: "Default basic BidRequest with empty bids for stored response" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid(bid: [], seat: GENERIC)) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: There must be at least one bid in stored response seatBid' + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should prefer seatbidobj over storedAuctionResponse.id from imp when both are present"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + id = PBSUtils.randomString + seatBidObject = storedAuctionResponse + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should set seatBids in response from multiple imp.ext.prebid.storedBidResponse.seatbidobj when it is defined"() { + given: "BidRequest with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp = [impWithSeatBidObject, impWithSeatBidObject] + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response bids as requested" + assert convertToComparableSeatBid(response.seatbid).bid.flatten().sort() == + bidRequest.imp.ext.prebid.storedAuctionResponse.seatBidObject.bid.flatten().sort() + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should prefer seatbidarr from request over seatbidobj from imp when both are present"() { + given: "Default basic BidRequest with stored response" + def bidRequest = BidRequest.defaultBidRequest + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + bidRequest.tap{ + imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { + seatBidObject = SeatBid.getStoredResponse(bidRequest) + } + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBids: [storedAuctionResponse]) + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain same stored auction response as requested" + assert response.seatbid == [storedAuctionResponse] + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + private static final Imp getImpWithSeatBidObject() { + def imp = Imp.defaultImpression + def bids = Bid.getDefaultBids([imp]) + def seatBid = new SeatBid(bid: bids, seat: GENERIC) + imp.tap { + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: seatBid) + } + } + + private static final List convertToComparableSeatBid(List seatBid) { + seatBid*.tap { + it.bid*.ext = null + it.group = null + } + } } diff --git a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java index b284cf60f3a..adcd1e9a296 100644 --- a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java @@ -126,7 +126,7 @@ public void skipAuctionShouldReturnFailedFutureWhenStoredResponseSeatBidAndIdAre final AuctionContext auctionContext = AuctionContext.builder() .bidRequest(BidRequest.builder() .ext(ExtRequest.of(ExtRequestPrebid.builder() - .storedAuctionResponse(ExtStoredAuctionResponse.of(null, null)) + .storedAuctionResponse(ExtStoredAuctionResponse.of(null, null, null)) .build())) .build()) .build(); @@ -147,7 +147,7 @@ public void skipAuctionShouldReturnFailedFutureWhenStoredResponseSeatBidAndIdAre public void skipAuctionShouldReturnBidResponseWithSeatBidsFromStoredAuctionResponse() { // given final List givenSeatBids = givenSeatBids("bidId1", "bidId2"); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -179,7 +179,8 @@ public void skipAuctionShouldReturnBidResponseWithSeatBidsFromStoredAuctionRespo @Test public void skipAuctionShouldReturnEmptySeatBidsWhenSeatBidIsNull() { // given - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", singletonList(null)); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of( + "id", singletonList(null), null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -214,7 +215,7 @@ public void skipAuctionShouldReturnEmptySeatBidsWhenSeatBidIsNull() { public void skipAuctionShouldReturnEmptySeatBidsWhenSeatIsEmpty() { // given final List givenSeatBids = singletonList(SeatBid.builder().seat("").build()); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -249,7 +250,7 @@ public void skipAuctionShouldReturnEmptySeatBidsWhenSeatIsEmpty() { public void skipAuctionShouldReturnEmptySeatBidsWhenBidsAreEmpty() { // given final List givenSeatBids = singletonList(SeatBid.builder().seat("seat").bid(emptyList()).build()); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", givenSeatBids, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .bidRequest(BidRequest.builder() @@ -283,7 +284,7 @@ public void skipAuctionShouldReturnEmptySeatBidsWhenBidsAreEmpty() { @Test public void skipAuctionShouldReturnBidResponseWithEmptySeatBidsWhenNoValueAvailableById() { // given - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .timeoutContext(TimeoutContext.of(1000L, timeout, 0)) @@ -320,7 +321,7 @@ public void skipAuctionShouldReturnBidResponseWithEmptySeatBidsWhenNoValueAvaila public void skipAuctionShouldReturnBidResponseWithStoredSeatBidsByProvidedId() { // given final List givenSeatBids = givenSeatBids("bidId1", "bidId2"); - final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null); + final ExtStoredAuctionResponse givenStoredResponse = ExtStoredAuctionResponse.of("id", null, null); final AuctionContext givenAuctionContext = AuctionContext.builder() .debugWarnings(new ArrayList<>()) .timeoutContext(TimeoutContext.of(1000L, timeout, 0)) diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java index 7b88fb26c34..3b625ddf379 100644 --- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java @@ -83,7 +83,7 @@ public void setUp() { @Test public void getStoredResponseResultShouldReturnSeatBidsForAuctionResponseId() throws JsonProcessingException { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.succeededFuture(StoredResponseDataResult.of(singletonMap("1", @@ -149,7 +149,7 @@ public void getStoredResponseResultShouldAddImpToRequiredRequestWhenItsStoredAuc @Test public void getStoredResponseResultShouldReturnFailedFutureWhenErrorHappenedDuringRetrievingStoredResponse() { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.failedFuture(new PreBidException("Failed."))); @@ -194,7 +194,7 @@ public void getStoredResponseResultShouldReturnResultForBidAndAuctionStoredRespo // given final Imp imp1 = givenImp( "impId1", - ExtStoredAuctionResponse.of("storedAuctionResponseId", Collections.emptyList()), + ExtStoredAuctionResponse.of("storedAuctionResponseId", Collections.emptyList(), null), null); final Imp imp2 = givenImp( "impId2", @@ -228,7 +228,7 @@ public void getStoredResponseResultShouldReturnResultForBidAndAuctionStoredRespo @Test public void getStoredResponseResultShouldThrowInvalidRequestExceptionWhenStoredAuctionResponseWasNotFound() { // given - final Imp imp1 = givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponseId", null), null); + final Imp imp1 = givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponseId", null, null), null); given(applicationSettings.getStoredResponses(any(), any())).willReturn( Future.succeededFuture(StoredResponseDataResult.of(emptyMap(), emptyList()))); @@ -247,8 +247,8 @@ public void getStoredResponseResultShouldThrowInvalidRequestExceptionWhenStoredA public void getStoredResponseResultShouldMergeStoredSeatBidsForTheSameBidder() throws JsonProcessingException { // given final List imps = asList( - givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponse1", null), null), - givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2", null), null)); + givenImp("impId1", ExtStoredAuctionResponse.of("storedAuctionResponse1", null, null), null), + givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2", null, null), null)); final Map storedResponse = new HashMap<>(); storedResponse.put("storedAuctionResponse1", mapper.writeValueAsString(asList( @@ -275,9 +275,65 @@ public void getStoredResponseResultShouldMergeStoredSeatBidsForTheSameBidder() t SeatBid.builder() .seat("rubicon") .bid(asList( - Bid.builder().id("id2").impid("impId2").build(), - Bid.builder().id("id3").impid("impId1").build() - )) + Bid.builder().id("id3").impid("impId1").build(), + Bid.builder().id("id2").impid("impId2").build())) + .build()), + emptyMap())); + } + + @Test + public void getStoredResponseResultShouldUseStoredSeatBidsFromRequest() throws JsonProcessingException { + // given + final List imps = asList( + givenImp( + "impId1", + ExtStoredAuctionResponse.of( + "storedAuctionResponse1", + null, + SeatBid.builder() + .seat("rubicon") + .bid(singletonList(Bid.builder().id("id4").build())) + .build()), + null), + givenImp("impId2", ExtStoredAuctionResponse.of("storedAuctionResponse2", null, null), null), + givenImp( + "impId3", + ExtStoredAuctionResponse.of( + null, + null, + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id5").build())) + .build()), + null)); + + final Map storedResponse = new HashMap<>(); + storedResponse.put("storedAuctionResponse1", mapper.writeValueAsString(asList( + SeatBid.builder().seat("appnexus").bid(singletonList(Bid.builder().id("id1").build())).build(), + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id3").build())).build()))); + storedResponse.put("storedAuctionResponse2", mapper.writeValueAsString(singletonList( + SeatBid.builder().seat("rubicon").bid(singletonList(Bid.builder().id("id2").build())).build()))); + + given(applicationSettings.getStoredResponses(any(), any())).willReturn( + Future.succeededFuture(StoredResponseDataResult.of(storedResponse, emptyList()))); + + // when + final Future result = + target.getStoredResponseResult(imps, timeout); + + // then + assertThat(result.result()).isEqualTo(StoredResponseResult.of( + emptyList(), + asList( + SeatBid.builder() + .seat("appnexus") + .bid(singletonList(Bid.builder().id("id5").impid("impId3").build())) + .build(), + SeatBid.builder() + .seat("rubicon") + .bid(asList( + Bid.builder().id("id4").impid("impId1").build(), + Bid.builder().id("id2").impid("impId2").build())) .build()), emptyMap())); } @@ -301,7 +357,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenSeatIsEmptyInStor throws JsonProcessingException { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.succeededFuture(StoredResponseDataResult.of( @@ -328,7 +384,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto // given final List imps = singletonList( - givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())) .willReturn(Future.succeededFuture(StoredResponseDataResult.of( @@ -352,7 +408,7 @@ public void getStoredResponseResultShouldReturnFailedFutureWhenBidsAreEmptyInSto @Test public void getStoredResponseResultShouldReturnFailedFutureSeatBidsCannotBeParsed() { // given - final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null), null)); + final List imps = singletonList(givenImp("impId", ExtStoredAuctionResponse.of("1", null, null), null)); given(applicationSettings.getStoredResponses(any(), any())).willReturn(Future.succeededFuture( StoredResponseDataResult.of(singletonMap("1", "{invalid"), emptyList()))); diff --git a/src/test/java/org/prebid/server/validation/ImpValidatorTest.java b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java index 04e5eb1f326..652150b6732 100644 --- a/src/test/java/org/prebid/server/validation/ImpValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/ImpValidatorTest.java @@ -18,6 +18,7 @@ import com.iab.openrtb.request.TitleObject; import com.iab.openrtb.request.Video; import com.iab.openrtb.request.VideoObject; +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; @@ -42,6 +43,7 @@ import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -1347,7 +1349,7 @@ public void validateImpsShouldReturnValidationMessageWhenImpExtPrebidBiddersNotD // given final ObjectNode prebid = mapper.valueToTree(ExtImpPrebid.builder() .storedBidResponse(singletonList(ExtStoredBidResponse.of("bidder", "id"))) - .storedAuctionResponse(ExtStoredAuctionResponse.of("id", null)) + .storedAuctionResponse(ExtStoredAuctionResponse.of("id", null, null)) .build()); final List givenImps = singletonList(validImpBuilder() @@ -1573,14 +1575,28 @@ public void validateImpsShouldReturnValidationMessageWhenExtImpPrebidHasStoredAu // given final List givenImps = singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( - "storedauctionresponse", mapper.createObjectNode())))) + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( + "storedauctionresponse", mapper.createObjectNode())))) .build()); // when & then assertThatThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)) .isInstanceOf(ValidationException.class) - .hasMessage("request.imp[0].ext.prebid.storedauctionresponse.id should be defined"); + .hasMessage("request.imp[0].ext.prebid.storedauctionresponse.{id or seatbidobj} should be defined"); + } + + @Test + public void validateImpsShouldNotReturnValidationMessageWhenStoredAuctionResponseWithoutIdAndWithSeatBidObj() + throws ValidationException { + + // given + final List givenImps = singletonList(validImpBuilder() + .ext(mapper.valueToTree(singletonMap("prebid", singletonMap( + "storedauctionresponse", singletonMap("seatbidobj", SeatBid.builder().build()))))) + .build()); + + // when & then + assertThatNoException().isThrownBy(() -> target.validateImps(givenImps, Collections.emptyMap(), null)); } @Test @@ -1589,11 +1605,11 @@ public void validateImpsShouldReturnWarningMessageWhenExtImpPrebidHasStoredAucti // given final List givenImps = singletonList(validImpBuilder() - .ext(mapper.valueToTree(singletonMap("prebid", Map.of( - "storedauctionresponse", mapper.createObjectNode() - .put("id", "1") - .set("seatbidarr", mapper.createArrayNode()))) - )).build()); + .ext(mapper.valueToTree(singletonMap("prebid", Map.of( + "storedauctionresponse", mapper.createObjectNode() + .put("id", "1") + .set("seatbidarr", mapper.createArrayNode()))) + )).build()); final List debugMessages = new ArrayList<>(); From ecd509deda2c12864a2334b8aba465db3f31c786 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:01:45 +0200 Subject: [PATCH 084/170] Core: Remove Empty EIDs + add new ortb fields (#3465) --- .../java/com/iab/openrtb/request/Eid.java | 27 +-- .../java/com/iab/openrtb/request/Uid.java | 4 +- .../requestfactory/AmpRequestFactory.java | 1 + .../requestfactory/AuctionRequestFactory.java | 3 +- .../requestfactory/Ortb2RequestFactory.java | 45 ++++- .../requestfactory/VideoRequestFactory.java | 29 +-- .../server/bidder/rubicon/RubiconBidder.java | 7 +- .../spring/config/ServiceConfiguration.java | 2 - .../server/validation/RequestValidator.java | 14 -- .../model/request/auction/Eid.groovy | 8 + .../server/functional/tests/EidsSpec.groovy | 75 ++++++++ .../functional/tests/OrtbConverterSpec.groovy | 2 +- .../server/auction/ExchangeServiceTest.java | 32 ++-- .../enforcement/TcfEnforcementTest.java | 2 +- .../enforcement/mask/UserFpdTcfMaskTest.java | 8 +- .../requestfactory/AmpRequestFactoryTest.java | 2 + .../AuctionRequestFactoryTest.java | 2 + .../Ortb2RequestFactoryTest.java | 68 ++++++- .../VideoRequestFactoryTest.java | 2 + .../BidRequestOrtb26To25ConverterTest.java | 4 +- .../up/BidRequestOrtb25To26ConverterTest.java | 4 +- .../bidder/adnuntius/AdnuntiusBidderTest.java | 6 +- .../bidder/rubicon/RubiconBidderTest.java | 167 ++++++++++-------- .../siverpush/SilverPushBidderTest.java | 6 +- .../validation/RequestValidatorTest.java | 59 +------ 25 files changed, 357 insertions(+), 222 deletions(-) diff --git a/src/main/java/com/iab/openrtb/request/Eid.java b/src/main/java/com/iab/openrtb/request/Eid.java index f8a728e93cb..04892b57cc6 100644 --- a/src/main/java/com/iab/openrtb/request/Eid.java +++ b/src/main/java/com/iab/openrtb/request/Eid.java @@ -1,33 +1,24 @@ package com.iab.openrtb.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; import java.util.List; -/** - * Extended identifiers support in the OpenRTB specification allows buyers - * to use audience data in real-time bidding. This object can contain one - * or more {@link Uid}s from a single source or a technology provider. The - * exchange should ensure that business agreements allow for the sending - * of this data. - */ -@Value(staticConstructor = "of") +@Value +@Builder(toBuilder = true) public class Eid { - /** - * Source or technology provider responsible for the set of included IDs. Expressed as a top-level domain. - */ String source; - /** - * Array of extended ID {@link Uid} objects from the given source. - * Refer to 3.2.28 Extended Identifier UIDs - */ List uids; - /** - * Placeholder for vendor specific extensions to this object - */ + String inserter; + + String matcher; + + Integer mm; + ObjectNode ext; } diff --git a/src/main/java/com/iab/openrtb/request/Uid.java b/src/main/java/com/iab/openrtb/request/Uid.java index 536d5a1e02b..c90e563b9d5 100644 --- a/src/main/java/com/iab/openrtb/request/Uid.java +++ b/src/main/java/com/iab/openrtb/request/Uid.java @@ -1,6 +1,7 @@ package com.iab.openrtb.request; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; /** @@ -8,7 +9,8 @@ * extended identifiers. The exchange should ensure that business * agreements allow for the sending of this data. */ -@Value(staticConstructor = "of") +@Value +@Builder(toBuilder = true) public class Uid { /** diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index b5ac7cb4705..0084afc7aca 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -410,6 +410,7 @@ private Future updateBidRequest(AuctionContext auctionContext) { .map(this::fillExplicitParameters) .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())) .compose(resolvedBidRequest -> ortb2RequestFactory.validateRequest( resolvedBidRequest, auctionContext.getHttpRequest(), diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 516205a2af0..bd720a25f2b 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -248,7 +248,8 @@ private Future updateBidRequest(AuctionStoredResult auctionStoredRes .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, hasStoredBidRequest)) - .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)); + .map(bidRequest -> cookieDeprecationService.updateBidRequestDevice(bidRequest, auctionContext)) + .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())); } private static MetricName requestTypeMetric(BidRequest bidRequest) { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index bd7411f24d7..336f2b5f8f1 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -4,10 +4,13 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.ext.web.RoutingContext; @@ -32,7 +35,6 @@ import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; @@ -51,11 +53,11 @@ import org.prebid.server.model.UpdateResult; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; -import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; @@ -73,12 +75,14 @@ import org.prebid.server.validation.model.ValidationResult; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.TreeMap; import java.util.function.Function; +import java.util.stream.Stream; public class Ortb2RequestFactory { @@ -99,7 +103,6 @@ public class Ortb2RequestFactory { private final ApplicationSettings applicationSettings; private final IpAddressHelper ipAddressHelper; private final HookStageExecutor hookStageExecutor; - private final PriceFloorProcessor priceFloorProcessor; private final CountryCodeMapper countryCodeMapper; private final Metrics metrics; @@ -115,7 +118,6 @@ public Ortb2RequestFactory(int timeoutAdjustmentFactor, ApplicationSettings applicationSettings, IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, - PriceFloorProcessor priceFloorProcessor, CountryCodeMapper countryCodeMapper, Metrics metrics) { @@ -135,7 +137,6 @@ public Ortb2RequestFactory(int timeoutAdjustmentFactor, this.applicationSettings = Objects.requireNonNull(applicationSettings); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); - this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); this.countryCodeMapper = Objects.requireNonNull(countryCodeMapper); this.metrics = Objects.requireNonNull(metrics); } @@ -206,6 +207,40 @@ public Future validateRequest(BidRequest bidRequest, : Future.succeededFuture(bidRequest); } + public BidRequest removeEmptyEids(BidRequest bidRequest, List warnings) { + final User user = bidRequest.getUser(); + + if (user == null) { + return bidRequest; + } + + final List eids = Stream.ofNullable(user.getEids()) + .flatMap(Collection::stream) + .map(eid -> eid.toBuilder().uids(removeEmptyUids(eid, warnings)).build()) + .filter(eid -> CollectionUtils.isNotEmpty(eid.getUids())) + .toList(); + + if (CollectionUtils.isEmpty(eids) && CollectionUtils.isNotEmpty(user.getEids())) { + warnings.add("removed empty EID array"); + } + + final User modifiedUser = user.toBuilder().eids(CollectionUtils.isEmpty(eids) ? null : eids).build(); + return bidRequest.toBuilder().user(modifiedUser).build(); + } + + private List removeEmptyUids(Eid eid, List warnings) { + return CollectionUtils.emptyIfNull(eid.getUids()).stream() + .filter(uid -> { + if (StringUtils.isBlank(uid.getId())) { + warnings.add("removed EID %s due to empty ID".formatted(eid.getSource())); + return false; + } + + return true; + }) + .toList(); + } + public Future enrichBidRequestWithGeolocationData(AuctionContext auctionContext) { final BidRequest bidRequest = auctionContext.getBidRequest(); final Device device = bidRequest.getDevice(); diff --git a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java index 8b2533da606..8c4d5de8614 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/VideoRequestFactory.java @@ -108,19 +108,18 @@ public Future> fromRequest(RoutingContext routingC Endpoint.openrtb2_video, MetricName.video); return ortb2RequestFactory.executeEntrypointHooks(routingContext, body, initialAuctionContext) - .compose(httpRequest -> - createBidRequest(httpRequest) + .compose(httpRequest -> createBidRequest(httpRequest) + .map(bidRequest -> removeEmptyEids(bidRequest, initialAuctionContext.getDebugWarnings())) + .compose(bidRequest -> validateRequest( + bidRequest, + httpRequest, + initialAuctionContext.getDebugWarnings())) - .compose(bidRequest -> validateRequest( - bidRequest, - httpRequest, - initialAuctionContext.getDebugWarnings())) + .map(bidRequestWithErrors -> populatePodErrors( + bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - .map(bidRequestWithErrors -> populatePodErrors( - bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - - .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( - initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) + .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) .compose(auctionContext -> ortb2RequestFactory.fetchAccountWithoutStoredRequestLookup(auctionContext) .map(auctionContext::with)) @@ -154,6 +153,14 @@ public Future> fromRequest(RoutingContext routingC .map(auctionContext -> WithPodErrors.of(auctionContext, podErrors)); } + private WithPodErrors removeEmptyEids(WithPodErrors requestWithPodErrors, + List debugWarnings) { + + return WithPodErrors.of( + ortb2RequestFactory.removeEmptyEids(requestWithPodErrors.getData(), debugWarnings), + requestWithPodErrors.getPodErrors()); + } + private String extractAndValidateBody(RoutingContext routingContext) { final String body = routingContext.getBodyAsString(); if (body == null) { diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index dd54fc864d8..031f7590475 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -1230,7 +1230,7 @@ private static Eid prepareExtUserEid(Eid extUserEid) { .filter(Objects::nonNull) .map(RubiconBidder::cleanExtUserEidUidStype) .toList(); - return Eid.of(extUserEid.getSource(), extUserEidUids, extUserEid.getExt()); + return extUserEid.toBuilder().uids(extUserEidUids).build(); } private static Uid cleanExtUserEidUidStype(Uid extUserEidUid) { @@ -1242,10 +1242,7 @@ private static Uid cleanExtUserEidUidStype(Uid extUserEidUid) { final ObjectNode extUserEidUidExtCopy = extUserEidUidExt.deepCopy(); extUserEidUidExtCopy.remove(STYPE_FIELD); - return Uid.of( - extUserEidUid.getId(), - extUserEidUid.getAtype(), - extUserEidUidExtCopy); + return extUserEidUid.toBuilder().ext(extUserEidUidExtCopy).build(); } private RubiconUserExtRp rubiconUserExtRp(User user, ExtImpRubicon rubiconImpExt) { diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index d248264a59d..5676d7fd43e 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -385,7 +385,6 @@ Ortb2RequestFactory openRtb2RequestFactory( IpAddressHelper ipAddressHelper, HookStageExecutor hookStageExecutor, CountryCodeMapper countryCodeMapper, - PriceFloorProcessor priceFloorProcessor, Metrics metrics) { final List blocklistedAccounts = splitToList(blocklistedAccountsString); @@ -403,7 +402,6 @@ Ortb2RequestFactory openRtb2RequestFactory( applicationSettings, ipAddressHelper, hookStageExecutor, - priceFloorProcessor, countryCodeMapper, metrics); } diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index bece88c27ac..f596850955d 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -10,7 +10,6 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; @@ -572,19 +571,6 @@ private void validateUser(User user, Map aliases) throws Validat throw new ValidationException( "request.user.eids[%d] missing required field: \"source\"", index); } - final List eidUids = eid.getUids(); - if (CollectionUtils.isEmpty(eidUids)) { - throw new ValidationException( - "request.user.eids[%d].uids must contain at least one element", index); - } - for (int uidsIndex = 0; uidsIndex < eidUids.size(); uidsIndex++) { - final Uid uid = eidUids.get(uidsIndex); - if (StringUtils.isBlank(uid.getId())) { - throw new ValidationException( - "request.user.eids[%d].uids[%d] missing required field: \"id\"", index, - uidsIndex); - } - } } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy index 1b70bd08799..2cb344f6967 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Eid.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.request.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils @@ -10,11 +11,18 @@ class Eid { String source List uids + String inserter + String matcher + @JsonProperty("mm") + Integer matchMethod static Eid getDefaultEid(String source = PBSUtils.randomString) { new Eid().tap { it.source = source it.uids = [Uid.defaultUid] + it.inserter = PBSUtils.randomString + it.matcher = PBSUtils.randomString + it.matchMethod = PBSUtils.randomNumber } } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy index 339a2637472..529f9c9c961 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy @@ -17,10 +17,13 @@ import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class EidsSpec extends BaseSpec { + private static final String EMPTY_STRING = "" + def "PBS shouldn't populate user.id from user.ext data"() { given: "Default basic BidRequest with generic bidder" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -197,4 +200,76 @@ class EidsSpec extends BaseSpec { and: "Alias bidder should contain one eids" assert sortedEids[1].eids == [eid] } + + def "PBS should populate warning for one removed UID when invalid uidId"() { + given: "BidRequest with eids" + def sourceId = PBSUtils.randomString + def validUidId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: sourceId, + uids: [new Uid(id: invalidUidId), + new Uid(id: validUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids.uids.id.flatten() == [validUidId] + + and: "Bid response should contain warning" + assert bidResponse.ext.warnings[PREBID]?.code == [999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["removed EID ${sourceId} due to empty ID" as String] + + where: + invalidUidId << [EMPTY_STRING, null] + } + + def "PBS should populate warnings for removed UIDs and entire eids when requested invalid uidIds"() { + given: "BidRequest with eids" + def sourceId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: sourceId, + uids: [new Uid(id: invalidUidId), + new Uid(id: invalidUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + + and: "Bid response should contain warnings" + assert bidResponse.ext.warnings[PREBID]?.code == [999, 999, 999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["removed EID ${sourceId} due to empty ID" as String, + "removed EID ${sourceId} due to empty ID" as String, + "removed empty EID array" as String] + + where: + invalidUidId << [EMPTY_STRING, null] + } + + def "PBS shouldn't populate warning for UID when Uid id is valid"() { + given: "BidRequest with eids" + def validUidId = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt(eids: [new Eid(source: PBSUtils.randomString, + uids: [new Uid(id: validUidId)])])) + } + + when: "PBS processes auction" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids.uids.id.flatten() == [validUidId] + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy index 7eb388cf202..21bb80df135 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/OrtbConverterSpec.groovy @@ -180,7 +180,7 @@ class OrtbConverterSpec extends BaseSpec { } } - def "PBS should move eids to o user.ext.eids when adapter doesn't support ortb 2.6"() { + def "PBS should move eids to user.ext.eids when adapter doesn't support ortb 2.6"() { given: "Default bid request with user.eids" def defaultEids = [Eid.defaultEid] def bidRequest = BidRequest.defaultBidRequest.tap { diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index e058d43b0df..7eb1a351531 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -2063,7 +2063,7 @@ public void shouldPassUserDataAndExtDataOnlyForAllowedBidder() { final ObjectNode dataNode = mapper.createObjectNode().put("data", "value"); final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0); - final List eids = singletonList(Eid.of("eId", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("eId").uids(emptyList()).build()); final ExtUser extUser = ExtUser.builder().data(dataNode).build(); final List data = singletonList(Data.builder().build()); @@ -2112,50 +2112,44 @@ public void shouldPassUserDataAndExtDataOnlyForAllowedBidder() { public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceIgnoringCase() { testUserEidsPermissionFiltering( // given - asList( - Eid.of("source1", null, null), - Eid.of("source2", null, null)), + asList(Eid.builder().source("source1").build(), Eid.builder().source("source2").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("OtHeRbIdDeR"))), emptyMap(), // expected - singletonList(Eid.of("source2", null, null)) - ); + singletonList(Eid.builder().source("source2").build())); } @Test public void shouldNotFilterUserExtEidsWhenEidsPermissionDoesNotContainSourceIgnoringCase() { testUserEidsPermissionFiltering( // given - singletonList(Eid.of("source1", null, null)), + singletonList(Eid.builder().source("source1").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source2", singletonList("OtHeRbIdDeR"))), emptyMap(), // expected - singletonList(Eid.of("source1", null, null)) - ); + singletonList(Eid.builder().source("source1").build())); } @Test public void shouldNotFilterUserExtEidsWhenSourceAllowedForAllBiddersIgnoringCase() { testUserEidsPermissionFiltering( // given - singletonList(Eid.of("source1", null, null)), + singletonList(Eid.builder().source("source1").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("*"))), emptyMap(), // expected - singletonList(Eid.of("source1", null, null)) - ); + singletonList(Eid.builder().source("source1").build())); } @Test public void shouldNotFilterUserExtEidsWhenSourceAllowedForBidderIgnoringCase() { testUserEidsPermissionFiltering( // given - singletonList(Eid.of("source1", null, null)), + singletonList(Eid.builder().source("source1").build()), singletonList(ExtRequestPrebidDataEidPermissions.of("source1", singletonList("SoMeBiDdEr"))), emptyMap(), // expected - singletonList(Eid.of("source1", null, null)) - ); + singletonList(Eid.builder().source("source1").build())); } @Test @@ -2173,7 +2167,7 @@ public void shouldFilterUserExtEidsWhenBidderIsNotAllowedForSourceAndSetNullIfNo singletonList("otherBidder"))))) .build())) .user(User.builder() - .eids(singletonList(Eid.of("source1", null, null))) + .eids(singletonList(Eid.builder().source("source1").build())) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build())); @@ -2209,7 +2203,7 @@ public void shouldFilterUserExtEidsWhenBidderPermissionsGivenToBidderAliasOnly() singletonList("someBidderAlias"))))) .build())) .user(User.builder() - .eids(singletonList(Eid.of("source1", null, null))) + .eids(singletonList(Eid.builder().source("source1").build())) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build())); @@ -2245,7 +2239,7 @@ public void shouldFilterUserExtEidsWhenPermissionsGivenToBidderButNotForAlias() singletonList("someBidder"))))) .build())) .user(User.builder() - .eids(singletonList(Eid.of("source1", null, null))) + .eids(singletonList(Eid.builder().source("source1").build())) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build())); @@ -2309,7 +2303,7 @@ public void shouldMaskUserExtIfDataBiddersListIsEmpty() { final ObjectNode dataNode = mapper.createObjectNode().put("data", "value"); final Map bidderToGdpr = doubleMap("someBidder", 1, "missingBidder", 0); - final List eids = singletonList(Eid.of("eId", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("eId").uids(emptyList()).build()); final ExtUser extUser = ExtUser.builder().data(dataNode).build(); final BidRequest bidRequest = givenBidRequest(givenSingleImp(bidderToGdpr), diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java index e8222accf25..0dad839fc87 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java @@ -394,7 +394,7 @@ private static Device givenDeviceWithNoPrivacyData() { private static User givenUserWithPrivacyData() { return User.builder() .id("originalUser") - .eids(singletonList(Eid.of(null, null, null))) + .eids(singletonList(Eid.builder().build())) .geo(Geo.builder().build()) .build(); } diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java index 14be79446d5..850c362e0d8 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/mask/UserFpdTcfMaskTest.java @@ -72,9 +72,9 @@ public void maskUserShouldReturnExpectedResultWhenEidsMasked() { .kwarray(emptyList()) .data(emptyList()) .eids(asList( - Eid.of("1", null, null), - Eid.of("2", null, null), - Eid.of("3", null, null))) + Eid.builder().source("1").build(), + Eid.builder().source("2").build(), + Eid.builder().source("3").build())) .geo(Geo.builder().lon(-85.34321F).lat(189.342323F).build()) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build(); @@ -92,7 +92,7 @@ public void maskUserShouldReturnExpectedResultWhenEidsMasked() { .keywords("keywords") .kwarray(emptyList()) .data(emptyList()) - .eids(singletonList(Eid.of("2", null, null))) + .eids(singletonList(Eid.builder().source("2").build())) .geo(Geo.builder().lon(-85.34321F).lat(189.342323F).build()) .ext(ExtUser.builder().data(mapper.createObjectNode()).build()) .build()); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index bad5e98fe8d..7318d7906c2 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -154,6 +154,8 @@ public void setUp() { given(ortb2RequestFactory.restoreResultFromRejection(any())) .willAnswer(invocation -> Future.failedFuture((Throwable) invocation.getArgument(0))); given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(fpdResolver.resolveApp(any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index ab3fa774e0c..dedb325e241 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -158,6 +158,8 @@ public void setUp() { ((AuctionContext) invocation.getArgument(0)).getBidRequest())); given(ortb2RequestFactory.validateRequest(any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture((BidRequest) invocationOnMock.getArgument(0))); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); given(paramsResolver.resolve(any(), any(), any(), anyBoolean())) diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index 6da1084c895..b38c305cd2b 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -5,10 +5,13 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpServerRequest; @@ -38,7 +41,6 @@ import org.prebid.server.exception.UnauthorizedAccountException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; @@ -121,8 +123,6 @@ public class Ortb2RequestFactoryTest extends VertxTest { @Mock(strictness = LENIENT) private HookStageExecutor hookStageExecutor; @Mock - private PriceFloorProcessor priceFloorProcessor; - @Mock private CountryCodeMapper countryCodeMapper; @Mock private Metrics metrics; @@ -1639,6 +1639,67 @@ public void enrichBidRequestWithAccountAndPrivacyDataShouldNotSetDsaFromAccountW .isSameAs(regs); } + @Test + public void removeEmptyEidsShouldNotAddWarningWhenEidsAreEmpty() { + final User givenUser = User.builder().eids(emptyList()).build(); + final BidRequest givenBidRequest = givenBidRequest(request -> request.user(givenUser)); + final List warnings = new ArrayList<>(); + + final BidRequest actualBidRequest = target.removeEmptyEids(givenBidRequest, warnings); + + assertThat(actualBidRequest.getUser().getEids()).isNull(); + assertThat(warnings).isEmpty(); + } + + @Test + public void removeEmptyEidsShouldRemoveEidsWhenNoValidEidsLeft() { + final User givenUser = User.builder() + .eids(List.of( + Eid.builder().source("source1").uids(List.of( + Uid.builder().id("").build(), + Uid.builder().id("").build())).build(), + Eid.builder().source("source2").uids(List.of(Uid.builder().id("").build())).build(), + Eid.builder().source("source3").uids(emptyList()).build())) + .build(); + + final BidRequest givenBidRequest = givenBidRequest(request -> request.user(givenUser)); + final List warnings = new ArrayList<>(); + + final BidRequest actualBidRequest = target.removeEmptyEids(givenBidRequest, warnings); + + assertThat(actualBidRequest.getUser().getEids()).isNull(); + assertThat(warnings).containsExactlyInAnyOrder( + "removed EID source1 due to empty ID", + "removed EID source1 due to empty ID", + "removed EID source2 due to empty ID", + "removed empty EID array"); + } + + @Test + public void removeEmptyEidsShouldRemoveEmptyUidsOnly() { + final User givenUser = User.builder() + .eids(List.of( + Eid.builder().source("source1").uids(List.of( + Uid.builder().id("id1").build(), + Uid.builder().id("").build())).build(), + Eid.builder().source("source2").uids(List.of(Uid.builder().id("id2").build())).build(), + Eid.builder().source("source3").uids(List.of(Uid.builder().id("").build())).build())) + .build(); + + final BidRequest givenBidRequest = givenBidRequest(request -> request.user(givenUser)); + final List warnings = new ArrayList<>(); + + final BidRequest actualBidRequest = target.removeEmptyEids(givenBidRequest, warnings); + + assertThat(actualBidRequest.getUser().getEids()).containsExactlyInAnyOrder( + Eid.builder().source("source1").uids(List.of(Uid.builder().id("id1").build())).build(), + Eid.builder().source("source2").uids(List.of(Uid.builder().id("id2").build())).build()); + + assertThat(warnings).containsExactlyInAnyOrder( + "removed EID source1 due to empty ID", + "removed EID source3 due to empty ID"); + } + private void givenTarget(int timeoutAdjustmentFactor) { target = new Ortb2RequestFactory( timeoutAdjustmentFactor, @@ -1653,7 +1714,6 @@ private void givenTarget(int timeoutAdjustmentFactor) { applicationSettings, ipAddressHelper, hookStageExecutor, - priceFloorProcessor, countryCodeMapper, metrics); } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java index 5764566ea17..475b22a5d5b 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/VideoRequestFactoryTest.java @@ -105,6 +105,8 @@ public void setUp() { given(ortb2RequestFactory.updateTimeout(any())).willAnswer(invocation -> invocation.getArgument(0)); given(ortb2RequestFactory.activityInfrastructureFrom(any())) .willReturn(Future.succeededFuture()); + given(ortb2RequestFactory.removeEmptyEids(any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(ortbVersionConversionManager.convertToAuctionSupportedVersion(any())) .willAnswer(invocation -> invocation.getArgument(0)); diff --git a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java index bad068fcea4..4d0aca65fa5 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/down/BidRequestOrtb26To25ConverterTest.java @@ -131,7 +131,7 @@ public void convertShouldMoveUserData() { final BidRequest bidRequest = givenBidRequest(request -> request.user( User.builder() .consent("consent") - .eids(singletonList(Eid.of("source", emptyList(), null))) + .eids(singletonList(Eid.builder().source("source").uids(emptyList()).build())) .ext(mapper.convertValue(Map.of("someField", "someValue"), ExtUser.class)) .build())); @@ -148,7 +148,7 @@ public void convertShouldMoveUserData() { final ExtUser expectedUserExt = ExtUser.builder() .consent("consent") - .eids(singletonList(Eid.of("source", emptyList(), null))) + .eids(singletonList(Eid.builder().source("source").uids(emptyList()).build())) .build(); expectedUserExt.addProperty("someField", TextNode.valueOf("someValue")); assertThat(user) diff --git a/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java b/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java index 2f52722be16..db1fc112be7 100644 --- a/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java +++ b/src/test/java/org/prebid/server/auction/versionconverter/up/BidRequestOrtb25To26ConverterTest.java @@ -242,7 +242,7 @@ public void convertShouldNotChangeUserConsentIfPresent() { @Test public void convertShouldMoveUserExtEidsToUserEidsIfNotPresent() { // given - final List eids = singletonList(Eid.of("source", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("source").uids(emptyList()).build()); final BidRequest bidRequest = givenBidRequest(request -> request.user( User.builder().ext(ExtUser.builder().eids(eids).build()).build())); @@ -265,7 +265,7 @@ public void convertShouldMoveUserExtEidsToUserEidsIfNotPresent() { @Test public void convertShouldNotChangeUserEidsIfPresent() { // given - final List eids = singletonList(Eid.of("source", emptyList(), null)); + final List eids = singletonList(Eid.builder().source("source").uids(emptyList()).build()); final BidRequest bidRequest = givenBidRequest(request -> request.user( User.builder().eids(eids).ext(ExtUser.builder().eids(emptyList()).build()).build())); diff --git a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java index ac80138bbc0..c911aed14c5 100644 --- a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java @@ -268,8 +268,7 @@ public void makeHttpRequestsShouldPopulateMetaDataUsiFromUserIdWhenBothUidIdAndU request -> request.user(User.builder() .id("userId") .ext(ExtUser.builder() - .eids(List.of(Eid.of(null, - List.of(Uid.of("eidsId", null, null)), null))) + .eids(List.of(Eid.builder().uids(List.of(Uid.builder().id("eidsId").build())).build())) .build()) .build()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), @@ -294,8 +293,7 @@ public void makeHttpRequestsShouldPopulateMetaDataUsiWhenUserExtEidsUidIdPresent request -> request.user(User.builder() .id(null) .ext(ExtUser.builder() - .eids(List.of(Eid.of(null, - List.of(Uid.of("eidsId", null, null)), null))) + .eids(List.of(Eid.builder().uids(List.of(Uid.builder().id("eidsId").build())).build())) .build()) .build()), givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index 24db88e46fc..7658e5be17a 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -1962,10 +1962,10 @@ public void makeHttpRequestsShouldNotCreateUserIfVisitorAndConsentNotPresent() { public void makeHttpRequestsShouldUseGivenUserIdIfOtherExtUserFieldsPassed() { // given final ExtUser extUser = ExtUser.builder() - .eids(singletonList(Eid.of( - "liveramp.com", - singletonList(Uid.of("firstId", null, null)), - null))) + .eids(singletonList(Eid.builder() + .source("liveramp.com") + .uids(singletonList(Uid.builder().id("firstId").build())) + .build())) .build(); final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .id("userId") @@ -1989,13 +1989,21 @@ public void makeHttpRequestsShouldUseGivenUserIdIfOtherExtUserFieldsPassed() { public void makeHttpRequestsShouldCreateUserIdIfMissingFromFirstUidStypePpuid() { // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() - .eids(singletonList(Eid.of( - null, - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.valueToTree(Map.of("stype", "ppuid"))), - Uid.of("id3", null, mapper.valueToTree(Map.of("stype", "ppuid")))), - null))) + .eids(singletonList(Eid.builder() + .uids(asList( + Uid.builder() + .id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build(), + Uid.builder() + .id("id2") + .ext(mapper.valueToTree(Map.of("stype", "ppuid"))) + .build(), + Uid.builder() + .id("id3") + .ext(mapper.valueToTree(Map.of("stype", "ppuid"))) + .build())) + .build())) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2015,12 +2023,16 @@ public void makeHttpRequestsShouldNotCreateUserIdIfMissingWhenNoUidWithPpuidType // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - null, - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.valueToTree(Map.of("stype", "other")))), - null))) + .eids(singletonList(Eid.builder().uids(asList( + Uid.builder() + .id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build(), + Uid.builder() + .id("id2") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2038,15 +2050,28 @@ public void makeHttpRequestsShouldNotCreateUserIdIfMissingWhenNoUidWithPpuidType @Test public void makeHttpRequestsShouldRemoveStypesPpuidSha256emailDmp() { // given - final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() - .eids(singletonList(Eid.of( - "source", - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.valueToTree(Map.of("stype", "ppuid"))), - Uid.of("id3", null, mapper.valueToTree(Map.of("stype", "sha256email"))), - Uid.of("id4", null, mapper.valueToTree(Map.of("stype", "dmp")))), - null))) + final BidRequest bidRequest = givenBidRequest( + builder -> builder.user(User.builder() + .eids(singletonList(Eid.builder() + .source("source") + .uids(asList( + Uid.builder() + .id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))) + .build(), + Uid.builder() + .id("id2") + .ext(mapper.valueToTree(Map.of("stype", "ppuid"))) + .build(), + Uid.builder() + .id("id3") + .ext(mapper.valueToTree(Map.of("stype", "sha256email"))) + .build(), + Uid.builder() + .id("id4") + .ext(mapper.valueToTree(Map.of("stype", "dmp"))) + .build())) + .build())) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2059,14 +2084,15 @@ public void makeHttpRequestsShouldRemoveStypesPpuidSha256emailDmp() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(request -> request.getUser().getExt()).hasSize(1).element(0) .isEqualTo(ExtUser.builder() - .eids(singletonList(Eid.of( - "source", - asList( - Uid.of("id1", null, mapper.valueToTree(Map.of("stype", "other"))), - Uid.of("id2", null, mapper.createObjectNode()), - Uid.of("id3", null, mapper.createObjectNode()), - Uid.of("id4", null, mapper.createObjectNode())), - null))) + .eids(singletonList(Eid.builder() + .source("source") + .uids(asList( + Uid.builder().id("id1") + .ext(mapper.valueToTree(Map.of("stype", "other"))).build(), + Uid.builder().id("id2").ext(mapper.createObjectNode()).build(), + Uid.builder().id("id3").ext(mapper.createObjectNode()).build(), + Uid.builder().id("id4").ext(mapper.createObjectNode()).build())) + .build())) .build()); } @@ -2075,10 +2101,10 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithAdServerEidSourceIfExt // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of("id", null, null)), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder().id("id").build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2092,10 +2118,10 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithAdServerEidSourceIfExt .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(request -> request.getUser().getExt()) .containsOnly(ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of("id", null, null)), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder().id("id").build())) + .build())) .build()); } @@ -2104,13 +2130,13 @@ public void makeHttpRequestsShouldCreateUserExtEidsWithAdServerEidSource() { // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of( - "adServerUid", - null, - mapper.valueToTree(Map.of("rtiPartner", "TDID")))), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder() + .id("adServerUid") + .ext(mapper.valueToTree(Map.of("rtiPartner", "TDID"))) + .build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2125,13 +2151,13 @@ public void makeHttpRequestsShouldCreateUserExtEidsWithAdServerEidSource() { .extracting(request -> request.getUser().getExt()) .containsOnly(jacksonMapper.fillExtension( ExtUser.builder() - .eids(singletonList(Eid.of( - "adserver.org", - singletonList(Uid.of( - "adServerUid", - null, - mapper.valueToTree(Map.of("rtiPartner", "TDID")))), - null))) + .eids(singletonList(Eid.builder() + .source("adserver.org") + .uids(singletonList(Uid.builder() + .id("adServerUid") + .ext(mapper.valueToTree(Map.of("rtiPartner", "TDID"))) + .build())) + .build())) .build(), RubiconUserExt.builder().build())); } @@ -2141,8 +2167,8 @@ public void makeHttpRequestsShouldIgnoreLiverampIdIfMissingEidUidId() { // given final ExtUser extUser = ExtUser.builder() .eids(asList( - Eid.of("liveramp.com", null, null), - Eid.of("liveramp.com", emptyList(), null))) + Eid.builder().source("liveramp.com").build(), + Eid.builder().source("liveramp.com").uids(emptyList()).build())) .build(); final BidRequest bidRequest = givenBidRequest( builder -> builder.user(User.builder().ext(extUser).build()), @@ -2165,13 +2191,13 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithUnknownEidSource() { // given final BidRequest bidRequest = givenBidRequest(builder -> builder.user(User.builder() .ext(ExtUser.builder() - .eids(singletonList(Eid.of( - "unknownSource", - singletonList(Uid.of( - "id", - null, - mapper.valueToTree(Map.of("rtiPartner", "eidUidId")))), - null))) + .eids(singletonList(Eid.builder() + .source("unknownSource") + .uids(singletonList(Uid.builder() + .id("id") + .ext(mapper.valueToTree(Map.of("rtiPartner", "eidUidId"))) + .build())) + .build())) .build()) .build()), builder -> builder.video(Video.builder().build()), identity()); @@ -2185,10 +2211,13 @@ public void makeHttpRequestsShouldNotCreateUserExtTpIdWithUnknownEidSource() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .extracting(request -> request.getUser().getExt()) .containsOnly(ExtUser.builder() - .eids(singletonList(Eid.of( - "unknownSource", - singletonList(Uid.of("id", null, mapper.valueToTree(Map.of("rtiPartner", "eidUidId")))), - null))) + .eids(singletonList(Eid.builder() + .source("unknownSource") + .uids(singletonList(Uid.builder() + .id("id") + .ext(mapper.valueToTree(Map.of("rtiPartner", "eidUidId"))) + .build())) + .build())) .build()); } diff --git a/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java b/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java index 2197850e023..f734090592c 100644 --- a/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/siverpush/SilverPushBidderTest.java @@ -79,8 +79,10 @@ public void makeHttpRequestsShouldFailOnMissingPublisherId() { @Test public void makeHttpRequestsShouldPassEidsFromDataToExtEids() { // given - final List givenEids = - List.of(Eid.of("testSource", List.of(Uid.of("testUidId", 2, null)), null)); + final List givenEids = List.of(Eid.builder() + .source("testSource") + .uids(List.of(Uid.builder().id("testUidId").atype(2).build())) + .build()); final ObjectNode givenDataNode = mapper.createObjectNode(); givenDataNode.set("eids", mapper.valueToTree(givenEids)); final BidRequest bidRequest = givenBidRequest(requestBuilder -> requestBuilder diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index a195514fa70..29e12ea6b66 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -13,11 +13,10 @@ import com.iab.openrtb.request.Pmp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; -import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.request.Video; -import org.junit.jupiter.api.Test; 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; @@ -1126,7 +1125,7 @@ public void validateShouldReturnValidationMessageWhenEidHasEmptySource() { // given final BidRequest bidRequest = validBidRequestBuilder() .user(User.builder() - .eids(singletonList(Eid.of(null, null, null))) + .eids(singletonList(Eid.builder().build())) .build()) .build(); @@ -1138,60 +1137,6 @@ public void validateShouldReturnValidationMessageWhenEidHasEmptySource() { .containsOnly("request.user.eids[0] missing required field: \"source\""); } - @Test - public void validateShouldReturnValidationMessageWhenEidHasNoUids() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of("source", null, null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids must contain at least one element"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidUidsIsEmpty() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of("source", emptyList(), null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids must contain at least one element"); - } - - @Test - public void validateShouldReturnValidationMessageWhenEidUidIdIsMissing() { - // given - final BidRequest bidRequest = validBidRequestBuilder() - .user(User.builder() - .eids(singletonList(Eid.of( - "source", - singletonList(Uid.of(null, null, null)), - null))) - .build()) - .build(); - - // when - final ValidationResult result = target.validate(bidRequest, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .containsOnly("request.user.eids[0].uids[0] missing required field: \"id\""); - } - @Test public void validateShouldReturnValidationMessageWhenAliasNameEqualsToBidderItPointsOn() { // given From 3e8004725f645bc6930d9c3357bc0327289aba06 Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:56:32 +0300 Subject: [PATCH 085/170] Tests: Rename package name (#3481) --- .../ResponseCorrectionSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/groovy/org/prebid/server/functional/tests/module/{requestcorrenction => responsecorrenction}/ResponseCorrectionSpec.groovy (99%) diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy similarity index 99% rename from src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy rename to src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy index 2c6c4dfd146..589925b3e34 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/requestcorrenction/ResponseCorrectionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy @@ -1,4 +1,4 @@ -package org.prebid.server.functional.tests.module.requestcorrenction +package org.prebid.server.functional.tests.module.responsecorrenction import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration From 22455873adaa8707fe3f9c86b6ab6f0274c01e4e Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:59:39 +0200 Subject: [PATCH 086/170] Core: Fix ext.analytics population (#3483) --- .../reporter/AnalyticsReporterDelegator.java | 7 ++++--- .../reporter/AnalyticsReporterDelegatorTest.java | 14 +++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java index 28768d6734f..f51db7f8ead 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java +++ b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java @@ -421,12 +421,13 @@ private AuctionContext updateAuctionContext(AuctionContext context, String adapt if (modules != null && modules.containsKey(adapterName)) { final ObjectNode moduleConfig = modules.get(adapterName); if (moduleConfigContainsAdapterSpecificData(moduleConfig)) { - final JsonNode analyticsNode = Optional.ofNullable(context.getBidRequest()) + final ExtRequestPrebid extRequestPrebid = Optional.ofNullable(context.getBidRequest()) .map(BidRequest::getExt) .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getAnalytics) .orElse(null); + final JsonNode analyticsNode = extRequestPrebid != null ? extRequestPrebid.getAnalytics() : null; + if (analyticsNode != null && analyticsNode.isObject()) { final ObjectNode adapterNode = Optional.ofNullable((ObjectNode) analyticsNode.get(adapterName)) .orElse(mapper.mapper().createObjectNode()); @@ -439,7 +440,7 @@ private AuctionContext updateAuctionContext(AuctionContext context, String adapt }); ((ObjectNode) analyticsNode).set(adapterName, adapterNode); - final ExtRequestPrebid updatedPrebid = ExtRequestPrebid.builder() + final ExtRequestPrebid updatedPrebid = extRequestPrebid.toBuilder() .analytics(analyticsNode) .build(); final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); diff --git a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java index cdced2f96f3..cc0a712b4e8 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java @@ -37,6 +37,7 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAnalyticsConfig; @@ -486,7 +487,10 @@ public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConf true, null, Map.of("logAnalytics", moduleConfig))) .build()) .bidRequest(BidRequest.builder() - .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(analyticsNode).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("channel")) + .analytics(analyticsNode) + .build())) .build()) .build(); @@ -503,6 +507,11 @@ public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConf expectedLogAnalyticsNode.put("property3", "requestValue3"); expectedAnalyticsNode.set("logAnalytics", expectedLogAnalyticsNode); + final ExtRequestPrebid expectedExtRequestPrebid = ExtRequestPrebid.builder() + .analytics(expectedAnalyticsNode) + .channel(ExtRequestPrebidChannel.of("channel")) + .build(); + final ArgumentCaptor auctionEventCaptor = ArgumentCaptor.forClass(AuctionEvent.class); verify(firstReporter).processEvent(auctionEventCaptor.capture()); assertThat(auctionEventCaptor.getValue()) @@ -510,8 +519,7 @@ public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConf .extracting(AuctionContext::getBidRequest) .extracting(BidRequest::getExt) .extracting(ExtRequest::getPrebid) - .extracting(ExtRequestPrebid::getAnalytics) - .isEqualTo(expectedAnalyticsNode); + .isEqualTo(expectedExtRequestPrebid); } @SuppressWarnings("unchecked") From 09d0049b6293ce513fba9afbe68f1e2b00836667 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Mon, 7 Oct 2024 14:48:54 +0200 Subject: [PATCH 087/170] Core: Default enabled property is true in analytics acc config (#3485) --- .../reporter/AnalyticsReporterDelegator.java | 8 +- .../functional/tests/AnalyticsSpec.groovy | 86 ++++++++++++------- .../AnalyticsReporterDelegatorTest.java | 25 ++++++ 3 files changed, 84 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java index f51db7f8ead..10db83bec8a 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java +++ b/src/main/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegator.java @@ -239,8 +239,12 @@ private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapt if (modules != null && modules.containsKey(adapter)) { final ObjectNode moduleConfig = modules.get(adapter); - return moduleConfig == null || !moduleConfig.has("enabled") - || !moduleConfig.get("enabled").asBoolean(); + + if (moduleConfig == null || !moduleConfig.has("enabled")) { + return false; + } + + return !moduleConfig.get("enabled").asBoolean(); } return !globalEnabledAdapters.contains(adapter); diff --git a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy index a16811b8261..b113f649834 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AnalyticsSpec.groovy @@ -62,22 +62,23 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS should call log analytics" + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "Analytics bid request shouldn't be emitted in logs" PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) - assert logsByValue - - and: "Analytics adapter shouldn't contain additional info" def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData } - def "PBS shouldn't populate log analytics when log enabled in account and disabled in global config"() { + def "PBS shouldn't populate log analytics when log analytics is directly non-restricted for account and disabled in global config"() { given: "Basic bid request" def bidRequest = BidRequest.defaultBidRequest and: "Account in the DB" - def logAnalyticsModule = new LogAnalytics(enabled: true) + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable) def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) def accountConfig = new AccountConfig(analytics: config) def account = new Account(uuid: bidRequest.accountId, config: accountConfig) @@ -86,17 +87,24 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + then: "PBS shouldn't call log analytics" def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) assert !logsByValue + + where: + logAnalyticsEnable << [null, true] } - def "PBS should populate log analytics when log enabled in account and global config"() { + def "PBS should populate log analytics when log analytics is directly non-restricted for account and enabled global config"() { given: "Basic bid request" def bidRequest = BidRequest.defaultBidRequest and: "Account in the DB" - def logAnalyticsModule = new LogAnalytics(enabled: true) + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable) def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) def accountConfig = new AccountConfig(analytics: config) def account = new Account(uuid: bidRequest.accountId, config: accountConfig) @@ -105,17 +113,17 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS should call log analytics" + then: "Analytics bid request shouldn't be emitted in logs" PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) - assert logsByValue - - and: "Analytics adapter shouldn't contain additional info" def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) assert !analyticsBidRequest?.ext?.prebid?.analytics?.logAnalytics?.additionalData + + where: + logAnalyticsEnable << [null, true] } - def "PBS shouldn't populate log analytics when log disabled in account and enabled in global config"() { + def "PBS shouldn't populate log analytics when log analytics is directly restricted for account and enabled in global config"() { given: "Basic bid request" def bidRequest = BidRequest.defaultBidRequest @@ -129,12 +137,16 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS shouldn't call log analytics" + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "PBS shouldn't call log analytics" def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) assert !logsByValue } - def "PBS shouldn't populate log analytics when log disabled in global config and without account"() { + def "PBS shouldn't populate log analytics when log disabled in global config and not set for account"() { given: "Basic bid request" def bidRequest = BidRequest.defaultBidRequest @@ -147,12 +159,16 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithoutLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS shouldn't call log analytics" + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics + + and: "PBS shouldn't call log analytics" def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) assert !logsByValue } - def "PBS should populate log analytics with additional data when log enabled in account and data specified"() { + def "PBS should populate log analytics with additional data when log is directly non-restricted for account and data specified"() { given: "Basic bid request" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.analytics = new PrebidAnalytics() @@ -160,7 +176,7 @@ class AnalyticsSpec extends BaseSpec { and: "Account in the DB" def additionalData = PBSUtils.randomString - def logAnalyticsModule = new LogAnalytics(enabled: true, additionalData: additionalData) + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: additionalData) def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) def accountConfig = new AccountConfig(analytics: config) def account = new Account(uuid: bidRequest.accountId, config: accountConfig) @@ -169,17 +185,21 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS should call log analytics" + then: "Bidder request shouldn't contain additional field from logAnalytics" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.ext.prebid.analytics.logAnalytics + + then: "Analytics bid request should be emitted in logs" PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) - assert logsByValue - - and: "Analytics adapter should contain additional info" def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + + where: + logAnalyticsEnable << [null, true] } - def "PBS should populate log analytics with additional data from request when log enabled in account and data specified in request only"() { + def "PBS should populate log analytics with additional data from request when data specified in request only"() { given: "Basic bid request" def additionalData = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -187,7 +207,7 @@ class AnalyticsSpec extends BaseSpec { } and: "Account in the DB" - def logAnalyticsModule = new LogAnalytics(enabled: true, additionalData: null) + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: null) def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) def accountConfig = new AccountConfig(analytics: config) def account = new Account(uuid: bidRequest.accountId, config: accountConfig) @@ -196,14 +216,14 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS should call log analytics" + then: "Analytics bid request should be emitted in logs" PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) - assert logsByValue - - and: "Analytics adapter should contain additional info" def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == additionalData + + where: + logAnalyticsEnable << [null, true] } def "PBS should prioritize logAnalytics from request when data specified in account and request"() { @@ -215,7 +235,7 @@ class AnalyticsSpec extends BaseSpec { and: "Account in the DB" def accountAdditionalData = PBSUtils.randomString - def logAnalyticsModule = new LogAnalytics(enabled: true, additionalData: accountAdditionalData) + def logAnalyticsModule = new LogAnalytics(enabled: logAnalyticsEnable, additionalData: accountAdditionalData) def config = new AccountAnalyticsConfig(modules: new AnalyticsModule(logAnalytics: logAnalyticsModule)) def accountConfig = new AccountConfig(analytics: config) def account = new Account(uuid: bidRequest.accountId, config: accountConfig) @@ -224,14 +244,14 @@ class AnalyticsSpec extends BaseSpec { when: "PBS processes auction request" pbsServiceWithLogAnalytics.sendAuctionRequest(bidRequest) - then: "PBS should call log analytics" + then: "Analytics bid request should be emitted in logs" PBSUtils.waitUntil({ pbsServiceWithLogAnalytics.isContainLogsByValue(bidRequest.id) }) def logsByValue = pbsServiceWithLogAnalytics.getLogsByValue(bidRequest.id) - assert logsByValue - - and: "Analytics adapter should contain additional info" def analyticsBidRequest = extractResolvedRequestFromLog(logsByValue) assert analyticsBidRequest.ext.prebid.analytics.logAnalytics.additionalData == bidRequestAdditionalData + + where: + logAnalyticsEnable << [null, true] } private static BidRequest extractResolvedRequestFromLog(String logsByText) { diff --git a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java index cc0a712b4e8..4f81b761e59 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/AnalyticsReporterDelegatorTest.java @@ -426,6 +426,31 @@ public void shouldNotCallAnalyticsAdapterIfDisabledByAccount() { verify(firstReporter, never()).processEvent(auctionEventCaptor.capture()); } + @Test + public void shouldCallAnalyticsAdapterIfAdapterNodePresentButEnabledPropertyNotPresent() { + // given + final ObjectNode moduleConfig = mapper.createObjectNode(); + moduleConfig.put("property1", "value1"); + moduleConfig.put("property2", "value2"); + + final AuctionContext auctionContext = AuctionContext.builder() + .account(Account.builder() + .analytics(AccountAnalyticsConfig.of( + true, null, Map.of("logAnalytics", moduleConfig))) + .build()) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().analytics(mapper.createObjectNode()).build())) + .build()) + .build(); + + // when + target.processEvent(AuctionEvent.builder().auctionContext(auctionContext).build()); + + // then + verify(vertx, times(2)).runOnContext(any()); + verify(firstReporter).processEvent(any()); + } + @Test public void shouldUpdateAuctionEventWithPropertiesFromAdapterSpecificAccountConfig() { // given From d35b9344d01ad9f1d01a22d93d43e3f206ef947b Mon Sep 17 00:00:00 2001 From: serhiinahornyi Date: Tue, 8 Oct 2024 11:58:58 +0200 Subject: [PATCH 088/170] Prebid Server prepare release 3.13.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 33a26a74d4b..cd399faafc4 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.13.0-SNAPSHOT + 3.13.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index e82a3d761ca..6c7e0d0b6c2 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0-SNAPSHOT + 3.13.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 4a4fcfa80da..fffae797600 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0-SNAPSHOT + 3.13.0 fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 6eac58c5ba0..51b74d4e627 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0-SNAPSHOT + 3.13.0 ortb2-blocking diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index a84008ae0d4..a4fab301f8a 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0-SNAPSHOT + 3.13.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 4bf4b87f902..26d7a0d649d 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0-SNAPSHOT + 3.13.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index b7eac66e702..ce57bb7058b 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.13.0-SNAPSHOT + 3.13.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 25987cb8260..a9b743801ed 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.13.0-SNAPSHOT + 3.13.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.13.0 diff --git a/pom.xml b/pom.xml index 5d8987c87bd..cb52d356d9d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.13.0-SNAPSHOT + 3.13.0 extra/pom.xml From e00b80c72da018409099aed73e082efd4c294e98 Mon Sep 17 00:00:00 2001 From: serhiinahornyi Date: Tue, 8 Oct 2024 11:58:58 +0200 Subject: [PATCH 089/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index cd399faafc4..eb64da6bd91 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.13.0 + 3.14.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 6c7e0d0b6c2..263056107ba 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0 + 3.14.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index fffae797600..3d14e3e6b51 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0 + 3.14.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 51b74d4e627..d997a0dc4bd 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0 + 3.14.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index a4fab301f8a..81fa2877d71 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0 + 3.14.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 26d7a0d649d..f3d73d09347 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.13.0 + 3.14.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index ce57bb7058b..438a800851c 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.13.0 + 3.14.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index a9b743801ed..123c70cbb3c 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.13.0 + 3.14.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.13.0 + HEAD diff --git a/pom.xml b/pom.xml index cb52d356d9d..75a749fab00 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.13.0 + 3.14.0-SNAPSHOT extra/pom.xml From 68142ccc855c4a300395d8e74016c532f3693cc6 Mon Sep 17 00:00:00 2001 From: ym-winston <46379634+ym-winston@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:32:00 -0400 Subject: [PATCH 090/170] Yieldmo: Update ortb-version to 2.6 (#3497) --- src/main/resources/bidder-config/yieldmo.yaml | 1 + .../server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/resources/bidder-config/yieldmo.yaml b/src/main/resources/bidder-config/yieldmo.yaml index 384804a3722..84415ead13e 100644 --- a/src/main/resources/bidder-config/yieldmo.yaml +++ b/src/main/resources/bidder-config/yieldmo.yaml @@ -1,6 +1,7 @@ adapters: yieldmo: endpoint: https://ads.yieldmo.com/exchange/prebid-server + ortb-version: "2.6" meta-info: maintainer-email: prebid@yieldmo.com app-media-types: diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json index 07adc0c136b..d4dafeeac95 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-yieldmo-bid-request.json @@ -36,9 +36,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { From b038c5d8f7981b5c0e2d9e7a68bea16052364942 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:39:58 +0200 Subject: [PATCH 091/170] InMobi: Mtype Support (#3477) --- .../server/bidder/inmobi/InmobiBidder.java | 32 +++--- .../bidder/inmobi/InmobiBidderTest.java | 100 ++++++++++-------- .../inmobi/test-auction-inmobi-response.json | 1 + .../inmobi/test-inmobi-bid-response.json | 3 +- 4 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java b/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java index 4a32a504fb8..33e8d25e9d7 100644 --- a/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/inmobi/InmobiBidder.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; @@ -95,40 +96,37 @@ private Imp updateImp(Imp imp) { public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList()); + return Result.of(extractBids(bidResponse), Collections.emptyList()); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } - return bidsFromResponse(bidRequest, bidResponse); + return bidsFromResponse(bidResponse); } - private List bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) { + private List bidsFromResponse(BidResponse bidResponse) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur())) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) .toList(); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getXNative() != null) { - return BidType.xNative; - } - } - } - return BidType.banner; + private static BidType getBidType(Bid bid) { + final Integer mtype = bid.getMtype(); + return switch (mtype) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unsupported mtype %d for bid %s" + .formatted(mtype, bid.getId())); + }; } } diff --git a/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java b/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java index c642ff5a2c6..e15093f49d4 100644 --- a/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/inmobi/InmobiBidderTest.java @@ -5,8 +5,6 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Native; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -24,10 +22,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.Function; +import java.util.function.UnaryOperator; import static java.util.Collections.singletonList; -import static java.util.function.Function.identity; +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.prebid.server.proto.openrtb.ext.response.BidType.banner; @@ -152,7 +150,7 @@ public void makeHttpRequestsShouldUpdateOnlyFirstImpression() { @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given - final BidderCall httpCall = givenHttpCall(null, "invalid"); + final BidderCall httpCall = givenHttpCall("invalid"); // when final Result> result = target.makeBids(httpCall, null); @@ -167,8 +165,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { @Test public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(null, - mapper.writeValueAsString(null)); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); // when final Result> result = target.makeBids(httpCall, null); @@ -181,8 +178,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(mapper.writeValueAsString(BidResponse.builder().build())); // when final Result> result = target.makeBids(httpCall, null); @@ -193,14 +189,9 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso } @Test - public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id(IMP_ID).banner(Banner.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(1))); // when final Result> result = target.makeBids(httpCall, null); @@ -208,17 +199,14 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), banner, null)); + .extracting(BidderBid::getType) + .containsExactly(banner); } @Test - public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(BidRequest.builder() - .imp(singletonList(Imp.builder().id(IMP_ID).video(Video.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(2))); // when final Result> result = target.makeBids(httpCall, null); @@ -226,18 +214,14 @@ public void makeBidsShouldReturnVideoBidIfVideoIsPresentInRequestImp() throws Js // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), video, null)); + .extracting(BidderBid::getType) + .containsExactly(video); } @Test - public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws JsonProcessingException { + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall( - BidRequest.builder() - .imp(singletonList(Imp.builder().id(IMP_ID).xNative(Native.builder().build()).build())) - .build(), - mapper.writeValueAsString( - givenBidResponse(bidBuilder -> bidBuilder.impid(IMP_ID)))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(4))); // when final Result> result = target.makeBids(httpCall, null); @@ -245,37 +229,69 @@ public void makeBidsShouldReturnNativeBidIfNativeIsPresentInRequestImp() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid(IMP_ID).build(), xNative, null)); + .extracting(BidderBid::getType) + .containsExactly(xNative); } - private static BidResponse givenBidResponse(Function bidCustomizer) { - return BidResponse.builder() - .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + @Test + public void makeBidsShouldReturnErrorOnInvalidBidType() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(identity())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Unsupported mtype null for bid bidId")); + } + + @Test + public void makeBidsShouldProceedWithErrorsAndValues() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid -> bid.mtype(3))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()) + .containsExactly(BidderError.badServerResponse("Unsupported mtype 3 for bid bidId")); + + assertThat(result.getValue()).isEmpty(); + } + + private static String givenBidResponse(UnaryOperator bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder().id("bidId")).build())) .build())) - .build(); + .build()); } - private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + private static BidderCall givenHttpCall(String body) { return BidderCall.succeededHttp( - HttpRequest.builder().payload(bidRequest).build(), + HttpRequest.builder().build(), HttpResponse.of(200, null, body), null); } - private static BidRequest givenBidRequest(Function impCustomizer) { + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { return givenBidRequest(identity(), impCustomizer); } private static BidRequest givenBidRequest( - Function bidRequestCustomizer, - Function impCustomizer) { + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { return bidRequestCustomizer.apply(BidRequest.builder() .imp(singletonList(givenImp(impCustomizer)))) .build(); } - private static Imp givenImp(Function impCustomizer) { + private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id(IMP_ID) .banner(Banner.builder().id("bannerId").build()) diff --git a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json index 0b00509195e..ea2a8cebd9e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json @@ -11,6 +11,7 @@ "adid": "adid001", "cid": "cid001", "crid": "crid001", + "mtype": 1, "w": 300, "h": 250, "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json index e291739474c..2769168e6ed 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-inmobi-bid-response.json @@ -11,10 +11,11 @@ "crid": "crid001", "cid": "cid001", "adm": "adm001", + "mtype": 1, "h": 250, "w": 300 } ] } ] -} \ No newline at end of file +} From def9b90e1a100e73907817112db435d48bd6ed23 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:41:57 +0200 Subject: [PATCH 092/170] Sonobi: Native and Currency Conversion Support (#3492) --- .../server/bidder/sonobi/SonobiBidder.java | 74 +++++++---- .../config/bidder/SonobiConfiguration.java | 4 +- src/main/resources/bidder-config/sonobi.yaml | 6 + .../bidder/sonobi/SonobiBidderTest.java | 120 ++++++++++++++++-- .../sonobi/test-auction-sonobi-response.json | 3 +- 5 files changed, 167 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java b/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java index 42ad63cbd5e..2582e8a5eea 100644 --- a/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java +++ b/src/main/java/org/prebid/server/bidder/sonobi/SonobiBidder.java @@ -3,16 +3,18 @@ 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; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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; @@ -22,6 +24,7 @@ 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; @@ -34,10 +37,17 @@ public class SonobiBidder implements Bidder { new TypeReference<>() { }; + private static final String BIDDER_CURRENCY = "USD"; + + private final CurrencyConversionService currencyConversionService; private final String endpointUrl; private final JacksonMapper mapper; - public SonobiBidder(String endpointUrl, JacksonMapper mapper) { + public SonobiBidder(CurrencyConversionService currencyConversionService, + String endpointUrl, + JacksonMapper mapper) { + + this.currencyConversionService = currencyConversionService; this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -50,7 +60,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ for (Imp imp : bidRequest.getImp()) { try { final ExtImpSonobi extImpSonobi = parseImpExt(imp); - final Imp modifiedImp = modifyImp(imp, extImpSonobi.getTagId()); + final Imp modifiedImp = modifyImp(bidRequest, imp, extImpSonobi.getTagId()); requests.add(makeRequest(bidRequest, modifiedImp)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -68,34 +78,51 @@ private ExtImpSonobi parseImpExt(Imp imp) throws PreBidException { } } - private static Imp modifyImp(Imp imp, String tagId) { - return imp.toBuilder().tagid(tagId).build(); + private Imp modifyImp(BidRequest bidRequest, Imp imp, String tagId) { + final Price bidFloor = resolveBidFloor(bidRequest, imp); + return imp.toBuilder() + .tagid(tagId) + .bidfloor(bidFloor.getValue()) + .bidfloorcur(bidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final BigDecimal bidFloor = imp.getBidfloor(); + final String bidFloorCurrency = imp.getBidfloorcur(); + + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) + && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { + return Price.of( + BIDDER_CURRENCY, + currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY)); + } + + return Price.of(bidFloorCurrency, bidFloor); } private HttpRequest makeRequest(BidRequest bidRequest, Imp imp) { - final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(Collections.singletonList(imp)).build(); + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .cur(Collections.singletonList(BIDDER_CURRENCY)) + .imp(Collections.singletonList(imp)) + .build(); return BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper); } @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final BidResponse bidResponse; try { - bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - } catch (DecodeException e) { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } - - final List errors = new ArrayList<>(); - final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); - - return Result.of(bids, errors); } private static List extractBids(BidRequest bidRequest, - BidResponse bidResponse, - List errors) { + BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); @@ -107,26 +134,19 @@ private static List extractBids(BidRequest bidRequest, .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(Objects::nonNull) - .map(bid -> makeBidderBid(bid, bidRequest.getImp(), bidResponse.getCur(), errors)) - .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), BIDDER_CURRENCY)) .toList(); } - private static BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { - try { - return BidderBid.of(bid, resolveBidType(bid.getImpid(), imps), currency); - } catch (PreBidException e) { - errors.add(BidderError.badServerResponse(e.getMessage())); - return null; - } - } - private static BidType resolveBidType(String impId, List imps) throws PreBidException { for (Imp imp : imps) { if (Objects.equals(impId, imp.getId())) { if (imp.getBanner() == null && imp.getVideo() != null) { return BidType.video; } + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() != null) { + return BidType.xNative; + } return BidType.banner; } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java index eaab8a70ef5..f640fe9f34d 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/SonobiConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.sonobi.SonobiBidder; +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; @@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps sonobiBidderDeps(BidderConfigurationProperties sonobiConfigurationProperties, + CurrencyConversionService currencyConversionService, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(sonobiConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new SonobiBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new SonobiBidder(currencyConversionService, config.getEndpoint(), mapper)) .assemble(); } } diff --git a/src/main/resources/bidder-config/sonobi.yaml b/src/main/resources/bidder-config/sonobi.yaml index 1c00983ec39..c6405555f1a 100644 --- a/src/main/resources/bidder-config/sonobi.yaml +++ b/src/main/resources/bidder-config/sonobi.yaml @@ -6,13 +6,19 @@ adapters: app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 104 usersync: cookie-family-name: sonobi + iframe: + url: https://sync.go.sonobi.com/uc.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&loc={{redirect_url}} + support-cors: false + uid-macro: '[UID]' redirect: url: https://sync.go.sonobi.com/us.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&loc={{redirect_url}} support-cors: false diff --git a/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java b/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java index 11bac67cb40..d5152ee00cc 100644 --- a/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/sonobi/SonobiBidderTest.java @@ -8,7 +8,11 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; 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; @@ -16,9 +20,11 @@ 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.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.sonobi.ExtImpSonobi; +import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -27,18 +33,31 @@ import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verifyNoInteractions; 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 SonobiBidderTest extends VertxTest { public static final String ENDPOINT_URL = "https://test.endpoint.com"; - private final SonobiBidder target = new SonobiBidder(ENDPOINT_URL, jacksonMapper); + @Mock + private CurrencyConversionService currencyConversionService; + + private SonobiBidder target; + + @BeforeEach + public void before() { + target = new SonobiBidder(currencyConversionService, ENDPOINT_URL, jacksonMapper); + } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new SonobiBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> + new SonobiBidder(currencyConversionService, "invalid_url", jacksonMapper)); } @Test @@ -60,7 +79,7 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldReturnExpectedBidRequest() { + public void makeHttpRequestsShouldReturnImpWithSetTagId() { // given final BidRequest bidRequest = givenBidRequest(identity()); @@ -68,14 +87,90 @@ public void makeHttpRequestsShouldReturnExpectedBidRequest() { final Result>> result = target.makeHttpRequests(bidRequest); // then - final BidRequest expectedRequest = bidRequest.toBuilder() - .imp(singletonList(bidRequest.getImp().getFirst().toBuilder() - .tagid("tagidString").build())) - .build(); assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .containsOnly(expectedRequest); + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsOnly("tagidString"); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasUSDCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, "USD")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasInvalidPrice() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.ZERO).bidfloorcur("GBR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ZERO, "GBR")); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotConvertBidfloorWhenBidfloorHasEmptyCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur(null)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, null)); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldConvertBidfloorToUSDWhenBidfloorHasAnotherCurrency() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("EUR")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "EUR", "USD")) + .willReturn(BigDecimal.ONE); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.ONE, "USD")); + } @Test @@ -204,7 +299,7 @@ public void makeBidsShouldReturnErrorsForBidsThatDoesNotMatchImp() throws JsonPr final Result> result = target.makeBids(httpCall, null); // then - assertThat(result.getValue()).containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).hasSize(1) .extracting(BidderError::getMessage) .containsExactly("Failed to find impression for ID: 456"); @@ -214,7 +309,10 @@ private static BidRequest givenBidRequest( Function impCustomizer, Function requestCustomizer) { - return requestCustomizer.apply(BidRequest.builder().imp(singletonList(givenImp(impCustomizer)))).build(); + return requestCustomizer.apply(BidRequest.builder() + .cur(singletonList("USD")) + .imp(singletonList(givenImp(impCustomizer)))) + .build(); } private static BidRequest givenBidRequest(Function impCustomizer) { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json index 2bf9c126798..8753fcde862 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json @@ -15,7 +15,8 @@ "prebid": { "type": "banner" }, - "origbidcpm": 1.25 + "origbidcpm": 1.25, + "origbidcur": "USD" } } ], From b5ec50bef3fd2cd83f4637452aff7a5324c04582 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:43:41 +0200 Subject: [PATCH 093/170] Core: Update TCF policy version validation (#3498) --- .../privacy/gdpr/TcfDefinerService.java | 16 +++-- .../VersionedVendorListService.java | 5 -- .../config/PrivacyServiceConfiguration.java | 6 +- .../scaffolding/VendorList.groovy | 3 + .../tests/privacy/GdprAmpSpec.groovy | 60 +++++++++++++++++-- .../tests/privacy/GdprAuctionSpec.groovy | 54 +++++++++++++++-- .../tests/privacy/PrivacyBaseSpec.groovy | 2 + .../util/privacy/VendorListConsent.groovy | 3 +- .../privacy/gdpr/TcfDefinerServiceTest.java | 34 +++++++---- .../VersionedVendorListServiceTest.java | 21 +------ 10 files changed, 151 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index 8836fbaa24c..b02698bbce3 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -66,6 +66,7 @@ public class TcfDefinerService { private final BidderCatalog bidderCatalog; private final IpAddressHelper ipAddressHelper; private final Metrics metrics; + private final double samplingRate; public TcfDefinerService(GdprConfig gdprConfig, Set eeaCountries, @@ -73,7 +74,8 @@ public TcfDefinerService(GdprConfig gdprConfig, GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, - Metrics metrics) { + Metrics metrics, + double samplingRate) { this.gdprEnabled = gdprConfig != null && BooleanUtils.isNotFalse(gdprConfig.getEnabled()); this.gdprDefaultValue = gdprConfig != null ? gdprConfig.getDefaultValue() : null; @@ -85,6 +87,7 @@ public TcfDefinerService(GdprConfig gdprConfig, this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.ipAddressHelper = Objects.requireNonNull(ipAddressHelper); this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; } /** @@ -360,11 +363,14 @@ private TCStringParsingResult toValidResult(String consentString, TCStringParsin } final int tcfPolicyVersion = tcString.getTcfPolicyVersion(); - // disable support for tcf policy version > 5 + // support for tcf policy version > 5 if (tcfPolicyVersion > 5) { - warnings.add("Parsing consent string: %s failed. TCF policy version %d is not supported".formatted( - consentString, tcfPolicyVersion)); - return TCStringParsingResult.of(TCStringEmpty.create(), warnings); + metrics.updateAlertsMetrics(MetricName.general); + + final String message = "Unknown tcfPolicyVersion %s, defaulting to gvlSpecificationVersion=3" + .formatted(tcfPolicyVersion); + UNDEFINED_CORRUPT_CONSENT_LOGGER.warn(message, samplingRate); + warnings.add(message); } return TCStringParsingResult.of(tcString, warnings); diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java index d0b6057e41a..5e261d9b6b4 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListService.java @@ -2,7 +2,6 @@ import com.iabtcf.decoder.TCString; import io.vertx.core.Future; -import org.prebid.server.exception.PreBidException; import org.prebid.server.privacy.gdpr.vendorlist.proto.Vendor; import java.util.Map; @@ -21,10 +20,6 @@ public VersionedVendorListService(VendorListService vendorListServiceV2, VendorL public Future> forConsent(TCString consent) { final int tcfPolicyVersion = consent.getTcfPolicyVersion(); final int vendorListVersion = consent.getVendorListVersion(); - if (tcfPolicyVersion > 5) { - return Future.failedFuture(new PreBidException( - "Invalid tcf policy version: %d".formatted(tcfPolicyVersion))); - } return tcfPolicyVersion < 4 ? vendorListServiceV2.forVersion(vendorListVersion) diff --git a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java index d9ac686d861..c7cac1ecf64 100644 --- a/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PrivacyServiceConfiguration.java @@ -164,7 +164,8 @@ TcfDefinerService tcfDefinerService( GeoLocationServiceWrapper geoLocationServiceWrapper, BidderCatalog bidderCatalog, IpAddressHelper ipAddressHelper, - Metrics metrics) { + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double samplingRate) { final Set eeaCountries = new HashSet<>(Arrays.asList(eeaCountriesAsString.trim().split(","))); @@ -175,7 +176,8 @@ TcfDefinerService tcfDefinerService( geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + samplingRate); } @Bean diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy index 3f84faa7c44..343a118f53a 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/VendorList.groovy @@ -10,6 +10,8 @@ import org.testcontainers.containers.MockServerContainer import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V2 +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.Vendor import static org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse.getDefaultVendorListResponse import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID @@ -46,6 +48,7 @@ class VendorList extends NetworkScaffolding { def prepareEncodeResponseBody = encode(defaultVendorListResponse.tap { it.tcfPolicyVersion = tcfPolicyVersion.vendorListVersion it.vendors = vendors + it.gvlSpecificationVersion = tcfPolicyVersion >= TcfPolicyVersion.TCF_POLICY_V4 ? V3 : V2 }) mockServerClient.when(request().withPath(prepareEndpoint), Times.unlimited(), TimeToLive.unlimited(), -10) diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy index 470029c9e02..d4cfc7bbda9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy @@ -26,6 +26,7 @@ import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 import static org.prebid.server.functional.model.config.PurposeEnforcement.BASIC import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS @@ -360,11 +361,12 @@ class GdprAmpSpec extends PrivacyBaseSpec { tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } - def "PBS amp with invalid consent.tcfPolicyVersion parameter should reject request and include proper warning"() { - given: "Tcf consent string" - def invalidTcfPolicyVersion = PBSUtils.getRandomNumber(5, 63) + def "PBS amp shouldn't reject request with proper warning and metrics when incoming consent.tcfPolicyVersion have invalid parameter"() { + given: "Tcf consent string with invalid tcf policy version" def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) .setTcfPolicyVersion(invalidTcfPolicyVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() and: "AMP request" @@ -377,13 +379,28 @@ class GdprAmpSpec extends PrivacyBaseSpec { def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) + and: "Flush metrics" + flushMetrics(privacyPbsService) + when: "PBS processes amp request" def response = privacyPbsService.sendAmpRequest(ampRequest) then: "Bid response should contain warning" assert response.ext?.warnings[PREBID]*.code == [999] assert response.ext?.warnings[PREBID]*.message == - ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] + ["Unknown tcfPolicyVersion ${invalidTcfPolicyVersion}, defaulting to gvlSpecificationVersion=3" as String] + + and: "Alerts.general metrics should be populated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["alerts.general"] == 1 + + and: "Bidder should be called" + assert bidder.getBidderRequest(ampStoredRequest.id) + + where: + invalidTcfPolicyVersion << [MIN_INVALID_TCF_POLICY_VERSION, + PBSUtils.getRandomNumber(MIN_INVALID_TCF_POLICY_VERSION, MAX_INVALID_TCF_POLICY_VERSION), + MAX_INVALID_TCF_POLICY_VERSION] } def "PBS amp should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { @@ -634,4 +651,39 @@ class GdprAmpSpec extends PrivacyBaseSpec { assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, TRANSMIT_PRECISE_GEO)] } + + def "PBS amp should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() { + given: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "AMP request" + def ampRequest = getGdprAmpRequest(tcfConsent) + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } + def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy index 72cd60c2b32..c4a21342ab9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy @@ -6,7 +6,6 @@ import org.prebid.server.functional.model.config.AccountGdprConfig import org.prebid.server.functional.model.config.PurposeConfig import org.prebid.server.functional.model.config.PurposeEnforcement import org.prebid.server.functional.model.request.auction.DistributionChannel -import org.prebid.server.functional.model.request.auction.RegsExt import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.container.PrebidServerContainer @@ -25,6 +24,7 @@ import static org.prebid.server.functional.model.config.Purpose.P1 import static org.prebid.server.functional.model.config.Purpose.P2 import static org.prebid.server.functional.model.config.Purpose.P4 import static org.prebid.server.functional.model.config.PurposeEnforcement.NO +import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3 import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA import static org.prebid.server.functional.model.pricefloors.Country.CAN import static org.prebid.server.functional.model.pricefloors.Country.USA @@ -314,11 +314,12 @@ class GdprAuctionSpec extends PrivacyBaseSpec { tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5] } - def "PBS auction should reject request with proper warning when incoming consent.tcfPolicyVersion have invalid parameter"() { - given: "Tcf consent string" - def invalidTcfPolicyVersion = PBSUtils.getRandomNumber(5, 63) + def "PBS auction shouldn't reject request with proper warning and metrics when incoming consent.tcfPolicyVersion have invalid parameter"() { + given: "Tcf consent string with invalid tcf policy version" def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) .setTcfPolicyVersion(invalidTcfPolicyVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) .build() and: "Bid request" @@ -333,7 +334,22 @@ class GdprAuctionSpec extends PrivacyBaseSpec { then: "Bid response should contain warning" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] assert response.ext?.warnings[ErrorType.PREBID]*.message == - ["Parsing consent string: ${tcfConsent} failed. TCF policy version ${invalidTcfPolicyVersion} is not supported" as String] + ["Unknown tcfPolicyVersion ${invalidTcfPolicyVersion}, defaulting to gvlSpecificationVersion=3" as String] + + and: "Alerts.general metrics should be populated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["alerts.general"] == 1 + + and: "Bid response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Bidder should be called" + assert bidder.getBidderRequest(bidRequest.id) + + where: + invalidTcfPolicyVersion << [MIN_INVALID_TCF_POLICY_VERSION, + PBSUtils.getRandomNumber(MIN_INVALID_TCF_POLICY_VERSION, MAX_INVALID_TCF_POLICY_VERSION), + MAX_INVALID_TCF_POLICY_VERSION] } def "PBS auction should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() { @@ -761,4 +777,32 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] assert !metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, TRANSMIT_PRECISE_GEO)] } + + def "PBS auction should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() { + given: "Tcf consent setup" + def tcfConsent = new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setTcfPolicyVersion(tcfPolicyVersion.value) + .setVendorListVersion(tcfPolicyVersion.vendorListVersion) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request" + def bidRequest = getGdprBidRequest(tcfConsent) + + and: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } + def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy index 7e5774829bd..145a33336f9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy @@ -103,6 +103,8 @@ abstract class PrivacyBaseSpec extends BaseSpec { protected static final String VALID_VALUE_FOR_GPC_HEADER = "1" protected static final GppConsent SIMPLE_GPC_DISALLOW_LOGIC = new UsNatV1Consent.Builder().setGpc(true).build() protected static final VendorList vendorListResponse = new VendorList(networkServiceContainer) + protected static final Integer MAX_INVALID_TCF_POLICY_VERSION = 63 + protected static final Integer MIN_INVALID_TCF_POLICY_VERSION = 6 @Shared protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG + diff --git a/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy b/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy index 6cdb42568ed..4ea50effbce 100644 --- a/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/privacy/VendorListConsent.groovy @@ -1,11 +1,12 @@ package org.prebid.server.functional.util.privacy import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion @JsonIgnoreProperties(ignoreUnknown = true) class VendorListConsent { Integer vendorListVersion Integer tcfPolicyVersion - Integer gvlSpecificationVersion + GvlSpecificationVersion gvlSpecificationVersion } diff --git a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java index 4b33f28c350..e63ad8f530d 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/TcfDefinerServiceTest.java @@ -85,7 +85,8 @@ public void setUp() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); } @Test @@ -99,7 +100,8 @@ public void resolveTcfContextShouldReturnContextWhenGdprIsDisabled() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); // when final Future result = target.resolveTcfContext( @@ -177,7 +179,8 @@ public void resolveTcfContextShouldCheckServiceConfigValueWhenRequestTypeIsUnkno geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final AccountGdprConfig accountGdprConfig = AccountGdprConfig.builder() .enabledForRequestType(EnabledForRequestType.of(true, true, true, true, true)) @@ -209,7 +212,8 @@ public void resolveTcfContextShouldConsiderTcfVersionOneAsCorruptedVersionTwo() geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = "BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA"; @@ -227,7 +231,7 @@ public void resolveTcfContextShouldConsiderTcfVersionOneAsCorruptedVersionTwo() } @Test - public void resolveTcfContextShouldTreatTcfConsentWithTcfPolicyVersionGreaterThanFourAsCorrupted() { + public void resolveTcfContextShouldEmitWarningOnTcfConsentWithTcfPolicyVersionGreaterThanFive() { // given final GdprConfig gdprConfig = GdprConfig.builder() .enabled(true) @@ -241,7 +245,8 @@ public void resolveTcfContextShouldTreatTcfConsentWithTcfPolicyVersionGreaterTha geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = TCStringEncoder.newBuilder() .version(2) @@ -260,11 +265,13 @@ public void resolveTcfContextShouldTreatTcfConsentWithTcfPolicyVersionGreaterTha null); // then - final String expectedWarning = "Parsing consent string: %s failed. TCF policy version 6 is not supported" - .formatted(vendorConsent); + final String expectedWarning = "Unknown tcfPolicyVersion 6, defaulting to gvlSpecificationVersion=3"; assertThat(result).isSucceeded(); - assertThat(result.result().getConsent()).isInstanceOf(TCStringEmpty.class); + assertThat(result.result().getConsent()) + .extracting(TCString::getVersion, TCString::getTcfPolicyVersion) + .containsExactly(2, 6); assertThat(result.result().getWarnings()).containsExactly(expectedWarning); + verify(metrics).updateAlertsMetrics(eq(MetricName.general)); } @Test @@ -282,7 +289,8 @@ public void resolveTcfContextShouldConsiderPresenceOfConsentStringAsInScope() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"; @@ -323,7 +331,8 @@ public void resolveTcfContextShouldUseEeaListFromAccountConfig() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); final String vendorConsent = "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"; @@ -442,7 +451,8 @@ public void resolveTcfContextShouldConsultDefaultValueWhenGeoLookupFailed() { geoLocationServiceWrapper, bidderCatalog, ipAddressHelper, - metrics); + metrics, + 0.01); given(geoLocationServiceWrapper.doLookup(anyString(), any(), any())).willReturn(Future.failedFuture("Bad ip")); diff --git a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java index 03a538d2f58..c6ae182fabc 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java @@ -11,7 +11,6 @@ import java.util.concurrent.ThreadLocalRandom; import static org.mockito.Mockito.verify; -import static org.prebid.server.assertion.FutureAssertion.assertThat; @ExtendWith(MockitoExtension.class) public class VersionedVendorListServiceTest { @@ -46,9 +45,9 @@ public void versionedVendorListServiceShouldTreatTcfPolicyLessThanFourAsVendorLi } @Test - public void versionedVendorListServiceShouldTreatTcfPolicyFourAsVendorListSpecificationThree() { + public void versionedVendorListServiceShouldTreatTcfPolicyGreaterOrEqualFourAsVendorListSpecificationThree() { // given - final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(4, 6); + final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(4, 100); final TCString consent = TCStringEncoder.newBuilder() .version(2) .tcfPolicyVersion(tcfPolicyVersion) @@ -61,20 +60,4 @@ public void versionedVendorListServiceShouldTreatTcfPolicyFourAsVendorListSpecif // then verify(vendorListServiceV3).forVersion(12); } - - @Test - public void versionedVendorListServiceShouldTreatTcfPolicyGreaterThanFourAsInvalidVersion() { - // given - final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(6, 63); - final TCString consent = TCStringEncoder.newBuilder() - .version(2) - .tcfPolicyVersion(tcfPolicyVersion) - .vendorListVersion(12) - .toTCString(); - - // when and then - assertThat(versionedVendorListService.forConsent(consent)) - .isFailed() - .hasMessage("Invalid tcf policy version: " + tcfPolicyVersion); - } } From 6cac67b5868b599ef56128835d46c462ba6c54dd Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:48:50 +0200 Subject: [PATCH 094/170] Missena: Add Bidder (#3501) --- .../bidder/missena/MissenaAdRequest.java | 25 ++ .../bidder/missena/MissenaAdResponse.java | 21 ++ .../server/bidder/missena/MissenaBidder.java | 157 ++++++++++ .../ext/request/missena/ExtImpMissena.java | 16 + .../config/bidder/MissenaConfiguration.java | 41 +++ src/main/resources/bidder-config/missena.yaml | 18 ++ .../static/bidder-params/missena.json | 24 ++ .../bidder/missena/MissenaBidderTest.java | 274 ++++++++++++++++++ .../org/prebid/server/it/MissenaTest.java | 35 +++ .../missena/test-auction-missena-request.json | 25 ++ .../test-auction-missena-response.json | 35 +++ .../missena/test-missena-bid-request.json | 10 + .../missena/test-missena-bid-response.json | 6 + .../server/it/test-application.properties | 6 +- 14 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java create mode 100644 src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java create mode 100644 src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java create mode 100644 src/main/resources/bidder-config/missena.yaml create mode 100644 src/main/resources/static/bidder-params/missena.json create mode 100644 src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/MissenaTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java new file mode 100644 index 00000000000..80bcba81fc6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -0,0 +1,25 @@ +package org.prebid.server.bidder.missena; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class MissenaAdRequest { + + String requestId; + + int timeout; + + String referer; + + String refererCanonical; + + String consentString; + + boolean consentRequired; + + String placement; + + String test; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java new file mode 100644 index 00000000000..6a34c31efbf --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdResponse.java @@ -0,0 +1,21 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Builder +@Value +public class MissenaAdResponse { + + String ad; + + BigDecimal cpm; + + String currency; + + @JsonProperty("requestId") + String requestId; +} diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java new file mode 100644 index 00000000000..913fd3b44b8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -0,0 +1,157 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MissenaBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final int AD_REQUEST_DEFAULT_TIMEOUT = 2000; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MissenaBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMissena extImp = parseImpExt(imp); + requests.add(makeHttpRequest(request, imp.getId(), extImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private ExtImpMissena parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing missenaExt parameters"); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, String impId, ExtImpMissena extImp) { + final Site site = request.getSite(); + + final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() + .requestId(request.getId()) + .timeout(AD_REQUEST_DEFAULT_TIMEOUT) + .referer(site == null ? null : site.getPage()) + .refererCanonical(site == null ? null : site.getDomain()) + .consentString(getUserConsent(request.getUser())) + .consentRequired(isGdpr(request.getRegs())) + .placement(extImp.getPlacement()) + .test(extImp.getTestMode()) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(extImp.getApiKey())) + .headers(makeHeaders(request.getDevice(), site)) + .impIds(Collections.singleton(impId)) + .body(mapper.encodeToBytes(missenaAdRequest)) + .payload(missenaAdRequest) + .build(); + } + + private MultiMap makeHeaders(Device device, Site site) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + } + + return headers; + } + + private String makeUrl(String apiKey) { + return endpointUrl + "?t=%s".formatted(apiKey); + } + + private static boolean isGdpr(Regs regs) { + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .map(gdpr -> gdpr == 1) + .orElse(false); + } + + private static String getUserConsent(User user) { + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(StringUtils.EMPTY); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final MissenaAdResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), + MissenaAdResponse.class); + return Result.withValues(Collections.singletonList(extractBid(bidRequest, bidResponse))); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private BidderBid extractBid(BidRequest request, MissenaAdResponse response) { + final Bid bid = Bid.builder() + .id(request.getId()) + .price(response.getCpm()) + .impid(request.getImp().getFirst().getId()) + .adm(response.getAd()) + .crid(response.getRequestId()) + .build(); + + return BidderBid.of(bid, BidType.banner, response.getCurrency()); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java new file mode 100644 index 00000000000..016e6ca99d5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMissena { + + @JsonProperty("apiKey") + String apiKey; + + String placement; + + @JsonProperty("test") + String testMode; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java new file mode 100644 index 00000000000..1c9c7fd355f --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.missena.MissenaBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/missena.yaml", factory = YamlPropertySourceFactory.class) +public class MissenaConfiguration { + + private static final String BIDDER_NAME = "missena"; + + @Bean("missenaConfigurationProperties") + @ConfigurationProperties("adapters.missena") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps missenaBidderDeps(BidderConfigurationProperties missenaConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(missenaConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MissenaBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/missena.yaml b/src/main/resources/bidder-config/missena.yaml new file mode 100644 index 00000000000..4f1ea9e4f8f --- /dev/null +++ b/src/main/resources/bidder-config/missena.yaml @@ -0,0 +1,18 @@ +adapters: + missena: + endpoint: https://bid.missena.io/ + meta-info: + maintainer-email: prebid@missena.com + modifying-vast-xml-allowed: true + app-media-types: + - banner + site-media-types: + - banner + supported-vendors: + vendor-id: 687 + usersync: + cookie-family-name: missena + iframe: + url: https://sync.missena.io/iframe?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/static/bidder-params/missena.json b/src/main/resources/static/bidder-params/missena.json new file mode 100644 index 00000000000..86bf5b45dec --- /dev/null +++ b/src/main/resources/static/bidder-params/missena.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Missena Adapter Params", + "description": "A schema which validates params accepted by the Missena adapter", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "API Key", + "minLength": 1 + }, + "placement": { + "type": "string", + "description": "Placement Type (Sticky, Header, ...)" + }, + "test": { + "type": "string", + "description": "Test Mode" + } + }, + "required": [ + "apiKey" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java new file mode 100644 index 00000000000..83326e4049f --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java @@ -0,0 +1,274 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singleton; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.prebid.server.util.HttpUtil.REFERER_HEADER; +import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER; +import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +class MissenaBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com"; + + private final MissenaBidder target = new MissenaBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Error parsing missenaExt parameters")); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("apiKey1", "plId1", "test1")), + imp -> imp.id("givenImp2").ext(givenImpExt("apiKey2", "plId2", "test2"))) + .toBuilder() + .id("requestId") + .site(Site.builder().page("page").domain("domain").build()) + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) + .build(); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + final MissenaAdRequest expectedRequest = MissenaAdRequest.builder() + .requestId("requestId") + .timeout(2000) + .referer("page") + .refererCanonical("domain") + .consentString("consent") + .consentRequired(true) + .build(); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .containsExactlyInAnyOrder( + expectedRequest.toBuilder().placement("plId1").test("test1").build(), + expectedRequest.toBuilder().placement("plId2").test("test2").build()); + } + + @Test + public void makeHttpRequestsShouldHaveImpIds() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(singleton("givenImp1"), singleton("givenImp2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIp() { + // given + final BidRequest bidRequest = givenBidRequest(identity()) + .toBuilder() + .site(Site.builder().page("page").build()) + .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)) + .isEqualTo("ua")) + .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)) + .isEqualTo("ip")) + .satisfies(headers -> assertThat(headers.get(REFERER_HEADER)) + .isEqualTo("page")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpv6Only() { + // given + final BidRequest bidRequest = givenBidRequest(identity()) + .toBuilder() + .device(Device.builder().ip(null).ipv6("ipv6").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE)) + .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)).isNull()) + .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)).isEqualTo("ipv6")) + .satisfies(headers -> assertThat(headers.get(REFERER_HEADER)).isNull()); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("impId2").ext(givenImpExt("apiKey", "placement", "testMode"))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final MissenaAdRequest expectedRequest = MissenaAdRequest.builder() + .timeout(2000) + .consentString("") + .consentRequired(false) + .test("testMode") + .placement("placement") + .build(); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .containsOnly(expectedRequest); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("apiKey", "plId", "test"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test-url.com?t=apiKey"); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1"), imp -> imp.id("impId2")) + .toBuilder().id("requestId").build(); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode"); + }); + } + + @Test + public void makeBidsShouldReturnSingleBid() throws JsonProcessingException { + // given + final MissenaAdResponse bidResponse = MissenaAdResponse.builder() + .requestId("id") + .cpm(BigDecimal.TEN) + .currency("USD") + .ad("adm") + .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1"), imp -> imp.id("impId2")) + .toBuilder().id("requestId").build(); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + final Bid expetedBid = Bid.builder() + .adm("adm") + .price(BigDecimal.TEN) + .crid("id") + .impid("impId1") + .id("requestId") + .build(); + + assertThat(result.getValue()).hasSize(1) + .containsOnly(BidderBid.of(expetedBid, BidType.banner, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(MissenaBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(givenImpExt("apikey", "placementId", "test"))) + .build(); + } + + private static ObjectNode givenImpExt(String apiKey, String placement, String testMode) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpMissena.of(apiKey, placement, testMode))); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/it/MissenaTest.java b/src/test/java/org/prebid/server/it/MissenaTest.java new file mode 100644 index 00000000000..e86227fb358 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MissenaTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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; +import static java.util.Collections.singletonList; + +public class MissenaTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMissena() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/missena-exchange")) + .withQueryParam("t", equalTo("apiKey")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/missena/test-missena-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/missena/test-missena-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/missena/test-auction-missena-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/missena/test-auction-missena-response.json", response, + singletonList("missena")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json new file mode 100644 index 00000000000..cacda5b188c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-request.json @@ -0,0 +1,25 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "missena": { + "apiKey": "apiKey", + "placement": "placement", + "test": "test" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json new file mode 100644 index 00000000000..d76e9f071e5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json @@ -0,0 +1,35 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "request_id", + "impid": "imp_id", + "price": 10.2, + "adm": "adm", + "crid": "id", + "ext": { + "origbidcpm": 10.2, + "origbidcur": "USD", + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "missena", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "missena": "{{ missena.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json new file mode 100644 index 00000000000..aa398c49b22 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json @@ -0,0 +1,10 @@ +{ + "request_id": "request_id", + "timeout": 2000, + "referer": "http://www.example.com", + "referer_canonical": "www.example.com", + "consent_string": "", + "consent_required": false, + "placement": "placement", + "test": "test" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json new file mode 100644 index 00000000000..4dece3dba9b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-response.json @@ -0,0 +1,6 @@ +{ + "requestId": "id", + "cpm": 10.2, + "ad": "adm", + "currency": "USD" +} 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 345445cc18e..9a4bd59f97e 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -313,6 +313,10 @@ adapters.mgid.enabled=true adapters.mgid.endpoint=http://localhost:8090/mgid-exchange/ adapters.mgidX.enabled=true adapters.mgidX.endpoint=http://localhost:8090/mgidx-exchange +adapters.minutemedia.enabled=true +adapters.minutemedia.endpoint=http://localhost:8090/minutemedia-exchange?publisherId={{PublisherId}} +adapters.missena.enabled=true +adapters.missena.endpoint=http://localhost:8090/missena-exchange adapters.mobfoxpb.enabled=true adapters.mobfoxpb.endpoint=http://localhost:8090/mobfoxpb-exchange?c=__route__&m=__method__&key=__key__ adapters.mobilefuse.enabled=true @@ -528,8 +532,6 @@ adapters.zmaticoo.enabled=true adapters.zmaticoo.endpoint=http://localhost:8090/zmaticoo-exchange adapters.yearxero.enabled=true adapters.yearxero.endpoint=http://localhost:8090/yearxero-exchange -adapters.minutemedia.enabled=true -adapters.minutemedia.endpoint=http://localhost:8090/minutemedia-exchange?publisherId={{PublisherId}} http-client.circuit-breaker.enabled=true http-client.circuit-breaker.idle-expire-hours=24 http-client.circuit-breaker.opening-threshold=1000 From c12606cc72a4ec91ba1d65d8d03c6cf6dd541a5a Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:50:01 +0200 Subject: [PATCH 095/170] Yieldlab: Support for Ad Unit Sizes (#3494) --- .../bidder/yieldlab/YieldlabBidder.java | 26 +++++++++++++++++++ .../ext/request/yieldlab/ExtImpYieldlab.java | 6 ----- .../static/bidder-params/yieldlab.json | 7 +---- .../bidder/yieldlab/YieldlabBidderTest.java | 19 +++++++------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index af1978d5206..cf99e41de84 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -49,6 +49,7 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -56,6 +57,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; public class YieldlabBidder implements Bidder { @@ -166,6 +168,12 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { .addParameter("ts", timestamp) .addParameter("t", getTargetingValues(extImpYieldlab)); + final String formats = makeFormats(request, extImpYieldlab); + + if (formats != null) { + uriBuilder.addParameter("sizes", formats); + } + final User user = request.getUser(); if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { uriBuilder.addParameter("ids", String.join("ylid:", user.getBuyeruid())); @@ -209,6 +217,24 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { return uriBuilder.toString(); } + private String makeFormats(BidRequest request, ExtImpYieldlab extImp) { + final List formats = new ArrayList<>(); + for (Imp imp: request.getImp()) { + if (isBanner(imp)) { + Stream.ofNullable(imp.getBanner().getFormat()) + .flatMap(Collection::stream) + .map(format -> "%s:%d|%d".formatted(extImp.getAdslotId(), format.getW(), format.getH())) + .forEach(formats::add); + } + } + + return formats.isEmpty() ? null : String.join(",", formats); + } + + private boolean isBanner(Imp imp) { + return imp.getBanner() != null && imp.getXNative() == null && imp.getVideo() == null && imp.getAudio() == null; + } + /** * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java index 28a4e036904..db165d6dd4e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldlab/ExtImpYieldlab.java @@ -6,9 +6,6 @@ import java.util.Map; -/** - * Defines the contract for bidrequest.imp[i].ext.yieldlab - */ @Builder @Value public class ExtImpYieldlab { @@ -19,9 +16,6 @@ public class ExtImpYieldlab { @JsonProperty("supplyId") String supplyId; - @JsonProperty("adSize") - String adSize; - Map targeting; @JsonProperty("extId") diff --git a/src/main/resources/static/bidder-params/yieldlab.json b/src/main/resources/static/bidder-params/yieldlab.json index 900d65da6e5..9d0fd0e88c0 100644 --- a/src/main/resources/static/bidder-params/yieldlab.json +++ b/src/main/resources/static/bidder-params/yieldlab.json @@ -12,10 +12,6 @@ "type": "string", "description": "Yieldlab ID of the supply" }, - "adSize": { - "type": "string", - "description": "Size of the adslot in pixel, e.g. 200x50" - }, "extId": { "type": "string", "description": "External ID used for reporting" @@ -27,7 +23,6 @@ }, "required": [ "adslotId", - "supplyId", - "adSize" + "supplyId" ] } diff --git a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java index c4ba2bdd8bb..a445a285955 100644 --- a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java @@ -7,6 +7,7 @@ import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; @@ -25,9 +26,9 @@ import org.prebid.server.bidder.yieldlab.model.YieldlabDigitalServicesActResponse; import org.prebid.server.bidder.yieldlab.model.YieldlabResponse; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; -import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.yieldlab.ExtImpYieldlab; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -100,12 +101,15 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { targeting.put("key2", "value2"); final BidRequest bidRequest = BidRequest.builder() .imp(singletonList(Imp.builder() - .banner(Banner.builder().w(1).h(1).build()) + .banner(Banner.builder() + .format(List.of( + Format.builder().w(1).h(1).build(), + Format.builder().w(2).h(2).build())) + .build()) .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(targeting) .extId("extId") .build()))) @@ -127,8 +131,8 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { .extracting(HttpRequest::getUri) .allSatisfy(uri -> { assertThat(uri).startsWith("https://test.endpoint.com/1?content=json&pvid=true&ts="); - assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&ids=buyeruid&yl_rtb_ifa&" - + "yl_rtb_devicetype=1&gdpr=1&consent=consent"); + assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&sizes=1%3A1%7C1%2C1%3A2%7C2&" + + "ids=buyeruid&yl_rtb_ifa&yl_rtb_devicetype=1&gdpr=1&consent=consent"); final String ts = uri.substring(54, uri.indexOf("&t=")); assertThat(Long.parseLong(ts)).isEqualTo(expectedTime); }); @@ -157,7 +161,6 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(targeting) .extId("extId") .build()))) @@ -168,7 +171,6 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { ExtImpYieldlab.builder() .adslotId("2") .supplyId("2") - .adSize("adSize") .targeting(targeting) .extId("extId") .build()))) @@ -220,7 +222,6 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(singletonMap("key", "value")) .extId("extId") .build()))) @@ -272,7 +273,6 @@ public void makeBidsShouldReturnCorrectAdm() throws JsonProcessingException { .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("12345") .supplyId("123456789") - .adSize("728x90") .extId("abc") .build()))) .video(Video.builder().build()) @@ -452,7 +452,6 @@ public void makeBidsShouldAddDsaParamsWhenDsaIsPresentInResponse() throws JsonPr .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() .adslotId("1") .supplyId("2") - .adSize("adSize") .targeting(singletonMap("key", "value")) .extId("extId") .build()))) From 351657db1df7e73c7839228894d2553d894ca305 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:53:49 +0200 Subject: [PATCH 096/170] Bidmatic: Add Bidder (#3489) --- .../bidder/bidmatic/BidmaticBidder.java | 176 ++++++++ .../bidder/bidmatic/BidmaticImpExt.java | 22 + .../ext/request/bidmatic/ExtImpBidmatic.java | 22 + .../config/bidder/BidmaticConfiguration.java | 41 ++ .../resources/bidder-config/bidmatic.yaml | 13 + .../static/bidder-params/bidmatic.json | 30 ++ .../bidder/bidmatic/BidmaticBidderTest.java | 382 ++++++++++++++++++ .../org/prebid/server/it/AdtelligentTest.java | 4 +- .../org/prebid/server/it/BidmaticTest.java | 35 ++ ...json => test-adtelligent-bid-request.json} | 0 ...son => test-adtelligent-bid-response.json} | 0 .../test-auction-bidmatic-request.json | 26 ++ .../test-auction-bidmatic-response.json | 37 ++ .../bidmatic/test-bidmatic-bid-request.json | 59 +++ .../bidmatic/test-bidmatic-bid-response.json | 20 + .../server/it/test-application.properties | 2 + 16 files changed, 867 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java create mode 100644 src/main/resources/bidder-config/bidmatic.yaml create mode 100644 src/main/resources/static/bidder-params/bidmatic.json create mode 100644 src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/BidmaticTest.java rename src/test/resources/org/prebid/server/it/openrtb2/adtelligent/{test-adtelligent-bid-request-1.json => test-adtelligent-bid-request.json} (100%) rename src/test/resources/org/prebid/server/it/openrtb2/adtelligent/{test-adtelligent-bid-response-1.json => test-adtelligent-bid-response.json} (100%) create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java new file mode 100644 index 00000000000..d425ebb9572 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticBidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.bidmatic; + +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; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.bidmatic.ExtImpBidmatic; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class BidmaticBidder implements Bidder { + + private static final TypeReference> EXT_IMP_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BidmaticBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final Map> sourceToImpsMap = new HashMap<>(); + + for (Imp imp : request.getImp()) { + final ExtImpBidmatic extImp; + try { + extImp = parseImpExt(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + + final int sourceId; + try { + sourceId = Integer.parseInt(extImp.getSourceId()); + } catch (NumberFormatException e) { + errors.add(BidderError.badInput("Cannot parse sourceId=%s to int".formatted(extImp.getSourceId()))); + continue; + } + + final Imp modifiedImp = modifyImp(imp, sourceId, extImp); + sourceToImpsMap.putIfAbsent(sourceId, new ArrayList<>()); + sourceToImpsMap.get(sourceId).add(modifiedImp); + } + + if (sourceToImpsMap.isEmpty()) { + return Result.withErrors(errors); + } + + sourceToImpsMap.forEach((sourceId, imps) -> requests.add(makeHttpRequest(request, sourceId, imps))); + return Result.of(requests, errors); + } + + private ExtImpBidmatic parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_IMP_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, Integer sourceId, ExtImpBidmatic extImp) { + final BidmaticImpExt modifiedExtImp = BidmaticImpExt.of( + sourceId, extImp.getPlacementId(), extImp.getSiteId(), extImp.getBidFloor()); + + return imp.toBuilder() + .bidfloor(BidderUtil.isValidPrice(extImp.getBidFloor()) ? extImp.getBidFloor() : imp.getBidfloor()) + .ext(mapper.mapper().createObjectNode().set("bidmatic", mapper.mapper().valueToTree(modifiedExtImp))) + .build(); + } + + private HttpRequest makeHttpRequest(BidRequest request, Integer sourceId, List imps) { + final BidRequest modifiedRequest = request.toBuilder().imp(imps).build(); + return BidderUtil.defaultRequest(modifiedRequest, makeUrl(sourceId), mapper); + } + + private String makeUrl(Integer sourceId) { + return endpointUrl + "?source=%d".formatted(sourceId); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final Map impMap = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, impMap, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, Map impMap, String currency, List errors) { + try { + final Pair bidType = getBidType(bid, impMap); + final Bid modifiedBid = bid.toBuilder().mtype(bidType.getRight()).build(); + return BidderBid.of(modifiedBid, bidType.getLeft(), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static Pair getBidType(Bid bid, Map impIdToImpMap) { + final Imp imp = impIdToImpMap.get(bid.getImpid()); + if (imp == null) { + throw new PreBidException("ignoring bid id=%s, request doesn't contain any impression with id=%s" + .formatted(bid.getId(), bid.getImpid())); + } + + if (imp.getBanner() != null) { + return Pair.of(BidType.banner, 1); + } else if (imp.getVideo() != null) { + return Pair.of(BidType.video, 2); + } else if (imp.getXNative() != null) { + return Pair.of(BidType.xNative, 4); + } else if (imp.getAudio() != null) { + return Pair.of(BidType.audio, 3); + } else { + return Pair.of(BidType.banner, 1); + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java new file mode 100644 index 00000000000..eead679d233 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidmatic/BidmaticImpExt.java @@ -0,0 +1,22 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class BidmaticImpExt { + + @JsonProperty("source") + Integer sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java new file mode 100644 index 00000000000..5823f02551e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmatic/ExtImpBidmatic.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.request.bidmatic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpBidmatic { + + @JsonProperty("source") + String sourceId; + + @JsonProperty("placementId") + Integer placementId; + + @JsonProperty("siteId") + Integer siteId; + + @JsonProperty("bidFloor") + BigDecimal bidFloor; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java new file mode 100644 index 00000000000..c5622397e71 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidmaticConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bidmatic.BidmaticBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/bidmatic.yaml", factory = YamlPropertySourceFactory.class) +public class BidmaticConfiguration { + + private static final String BIDDER_NAME = "bidmatic"; + + @Bean("bidmaticConfigurationProperties") + @ConfigurationProperties("adapters.bidmatic") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bidmaticBidderDeps(BidderConfigurationProperties bidmaticConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bidmaticConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BidmaticBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/bidmatic.yaml b/src/main/resources/bidder-config/bidmatic.yaml new file mode 100644 index 00000000000..47636ad0361 --- /dev/null +++ b/src/main/resources/bidder-config/bidmatic.yaml @@ -0,0 +1,13 @@ +adapters: + bidmatic: + endpoint: http://adapter.bidmatic.io/pbs/ortb + meta-info: + maintainer-email: advertising@bidmatic.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 1134 diff --git a/src/main/resources/static/bidder-params/bidmatic.json b/src/main/resources/static/bidder-params/bidmatic.json new file mode 100644 index 00000000000..65a1309dafa --- /dev/null +++ b/src/main/resources/static/bidder-params/bidmatic.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidmatic Adapter Params", + "description": "A schema which validates params accepted by the Bidmatic adapter", + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "source": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": [ + "source" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java new file mode 100644 index 00000000000..3e5891e02b2 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/bidmatic/BidmaticBidderTest.java @@ -0,0 +1,382 @@ +package org.prebid.server.bidder.bidmatic; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +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.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bidmatic.ExtImpBidmatic; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +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; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class BidmaticBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test-url.com"; + + private BidmaticBidder target; + + @BeforeEach + public void before() { + target = new BidmaticBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + // when and then + assertThatIllegalArgumentException().isThrownBy(() -> new BidmaticBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(bidderError -> { + assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(bidderError.getMessage()).startsWith("Cannot deserialize value"); + }); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerSourceId() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("1")), + imp -> imp.id("givenImp2").ext(givenImpExt("1")), + imp -> imp.id("givenImp3").ext(givenImpExt("2"))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(payload -> payload.getImp().stream().map(Imp::getId).collect(Collectors.toList())) + .containsExactlyInAnyOrder(List.of("givenImp1", "givenImp2"), List.of("givenImp3")); + } + + @Test + public void makeHttpRequestsShouldHaveImpIdsAndCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("1")), + imp -> imp.id("givenImp2").ext(givenImpExt("1")), + imp -> imp.id("givenImp3").ext(givenImpExt("2"))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds, HttpRequest::getUri) + .containsExactlyInAnyOrder( + tuple(Set.of("givenImp1", "givenImp2"), "https://test-url.com?source=1"), + tuple(Set.of("givenImp3"), "https://test-url.com?source=2")); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnImpWithUpdatedBidFloorWhenImpExtHasValidBidFloor() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").bidfloor(BigDecimal.TEN).ext(givenImpExt("1", BigDecimal.ONE)), + imp -> imp.id("givenImp2").bidfloor(BigDecimal.TEN).ext(givenImpExt("1", BigDecimal.ZERO))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId, Imp::getBidfloor) + .containsOnly(tuple("givenImp1", BigDecimal.ONE), tuple("givenImp2", BigDecimal.TEN)); + } + + @Test + public void makeHttpRequestsShouldReturnImpWithUpdatedExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("givenImp1").ext(givenImpExt("1", new BigDecimal("10.37")))); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getErrors()).isEmpty(); + + final ObjectNode expectedNode = mapper.createObjectNode(); + expectedNode.set("source", IntNode.valueOf(1)); + expectedNode.set("placementId", IntNode.valueOf(2)); + expectedNode.set("siteId", IntNode.valueOf(3)); + expectedNode.set("bidFloor", DecimalNode.valueOf(new BigDecimal("10.37"))); + final ObjectNode expectedImpExt = mapper.createObjectNode().set("bidmatic", expectedNode); + + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedImpExt); + } + + @Test + public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), + imp -> imp.id("impId2").ext(givenImpExt("string")), + imp -> imp.id("impId3")); + + //when + final Result>> result = target.makeHttpRequests(bidRequest); + + //then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("impId3"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenSourceIdCanNotBeParsedToInt() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("string"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).containsOnly(BidderError.badInput("Cannot parse sourceId=string to int")); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidResponseHasEmptySeatbids() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString( + BidResponse.builder().seatbid(emptyList()).build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").banner(Banner.builder().build())), + givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(bannerBid.toBuilder().mtype(1).build(), banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").video(Video.builder().build())), + givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(videoBid.toBuilder().mtype(2).build(), video, "USD")); + } + + @Test + public void makeBidsShouldReturnAudioBid() throws JsonProcessingException { + // given + final Bid audioBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").audio(Audio.builder().build())), + givenBidResponse(audioBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(audioBid.toBuilder().mtype(3).build(), audio, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBid() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(bid -> bid.id("bidId").impid("impId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").xNative(Native.builder().build())), + givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsOnly(BidderBid.of(nativeBid.toBuilder().mtype(4).build(), xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidIsNotInRequest() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(bid -> bid.id("bidId").impid("anotherImpId")); + final BidderCall httpCall = givenHttpCall( + givenBidRequest(imp -> imp.id("impId").xNative(Native.builder().build())), + givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).containsOnly(BidderError.badServerResponse( + "ignoring bid id=bidId, request doesn't contain any impression with id=anotherImpId")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(BidmaticBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .bidfloor(BigDecimal.TEN) + .bidfloorcur("USD") + .ext(mapper.valueToTree(ExtPrebid.of( + null, + ExtImpBidmatic.of("100", 1, 2, BigDecimal.TEN))))) + .build(); + } + + private static ObjectNode givenImpExt(String sourceId) { + return givenImpExt(sourceId, BigDecimal.TWO); + } + + private static ObjectNode givenImpExt(String sourceId, BigDecimal bidFloor) { + return mapper.valueToTree(ExtPrebid.of( + null, + ExtImpBidmatic.of(sourceId, 2, 3, bidFloor))); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .seatbid(singletonList(SeatBid.builder().bid(asList(bids)).build())) + .cur("USD") + .build()); + } + + private static Bid givenBid(UnaryOperator bidCustomizer) { + return bidCustomizer.apply(Bid.builder()).build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/it/AdtelligentTest.java b/src/test/java/org/prebid/server/it/AdtelligentTest.java index c46f87e83b8..b1122d4908d 100644 --- a/src/test/java/org/prebid/server/it/AdtelligentTest.java +++ b/src/test/java/org/prebid/server/it/AdtelligentTest.java @@ -19,9 +19,9 @@ public class AdtelligentTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromAdtelligent() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adtelligent-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-request-1.json"))) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-response-1.json")))); + jsonFrom("openrtb2/adtelligent/test-adtelligent-bid-response.json")))); // when final Response response = responseFor("openrtb2/adtelligent/test-auction-adtelligent-request.json", diff --git a/src/test/java/org/prebid/server/it/BidmaticTest.java b/src/test/java/org/prebid/server/it/BidmaticTest.java new file mode 100644 index 00000000000..b05011b5656 --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidmaticTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +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; +import static java.util.Collections.singletonList; + +public class BidmaticTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromBidmatic() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidmatic-exchange")) + .withQueryParam("source", equalTo("1000")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/bidmatic/test-bidmatic-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/bidmatic/test-bidmatic-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidmatic/test-auction-bidmatic-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/bidmatic/test-auction-bidmatic-response.json", response, + singletonList("bidmatic")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request-1.json rename to src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-request.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response-1.json rename to src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json new file mode 100644 index 00000000000..b58484a029d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-request.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidmatic": { + "source": 1000, + "siteId": 1234, + "bidFloor": 100, + "placementId": 10 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json new file mode 100644 index 00000000000..a45f9eeb3c9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json @@ -0,0 +1,37 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 8.43, + "adm": "adm14", + "crid": "crid14", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 8.43 + } + } + ], + "seat": "bidmatic", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "bidmatic": "{{ bidmatic.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json new file mode 100644 index 00000000000..a151c6c7cbd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-request.json @@ -0,0 +1,59 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "bidfloor": 100, + "ext": { + "bidmatic": { + "source": 1000, + "placementId": 10, + "siteId": 1234, + "bidFloor": 100 + } + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json new file mode 100644 index 00000000000..3dc3f71e392 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-bidmatic-bid-response.json @@ -0,0 +1,20 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 8.43, + "adm": "adm14", + "crid": "crid14", + "w": 300, + "h": 250 + } + ], + "seat": "bidmatic", + "group": 0 + } + ] +} 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 9a4bd59f97e..2ad3dec0705 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -484,6 +484,8 @@ adapters.between.enabled=true adapters.between.endpoint=http://localhost:8090/between-exchange?pubId={{PublisherId}} adapters.bidmachine.enabled=true adapters.bidmachine.endpoint=http://localhost:8090/bidmachine-exchange +adapters.bidmatic.enabled=true +adapters.bidmatic.endpoint=http://localhost:8090/bidmatic-exchange adapters.bidmyadz.enabled=true adapters.bidmyadz.endpoint=http://localhost:8090/bidmyadz-exchange adapters.unruly.enabled=true From f02440b379c57f6f421bed5d8709bc6f93fe0d6e Mon Sep 17 00:00:00 2001 From: Irakli Gotsiridze Date: Tue, 15 Oct 2024 17:04:18 +0400 Subject: [PATCH 097/170] Sovrn: Accept Imp.ext Bidfloor either as a number or string (#3484) --- src/main/resources/static/bidder-params/sovrn.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/resources/static/bidder-params/sovrn.json b/src/main/resources/static/bidder-params/sovrn.json index 803a8e127a1..4f779a9f1f6 100644 --- a/src/main/resources/static/bidder-params/sovrn.json +++ b/src/main/resources/static/bidder-params/sovrn.json @@ -13,8 +13,16 @@ "description": "An ID which identifies the sovrn ad tag (DEPRECATED, use \"tagid\" instead)" }, "bidfloor": { - "type": "number", - "description": "The minimum acceptable bid, in CPM, using US Dollars" + "anyOf": [ + { + "type": "number", + "description": "The minimum acceptable bid, in CPM, using US Dollars" + }, + { + "type": "string", + "description": "The minimum acceptable bid, in CPM, using US Dollars (as a string)" + } + ] }, "adunitcode": { "type": "string", From 2dfd97a2a3c86d3a1695e79e9d74093089cda50e Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Wed, 16 Oct 2024 12:45:20 +0300 Subject: [PATCH 098/170] Response correction: Fix VAST matching (#3493) --- .../appvideohtml/AppVideoHtmlCorrection.java | 5 +- .../AppVideoHtmlCorrectionTest.java | 2 +- .../ResponseCorrectionSpec.groovy | 82 ++++++++++++++++++- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java index 3df769e52cb..5b3ee918e86 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrection.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; @@ -29,7 +30,7 @@ public class AppVideoHtmlCorrection implements Correction { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( LoggerFactory.getLogger(AppVideoHtmlCorrection.class)); - private static final Pattern VAST_XML_PATTERN = Pattern.compile("<\\w*VAST\\w+", Pattern.CASE_INSENSITIVE); + private static final Pattern VAST_XML_PATTERN = Pattern.compile(".*<\\s*VAST\\s+.*", Pattern.CASE_INSENSITIVE); private static final TypeReference> EXT_BID_PREBID_TYPE_REFERENCE = new TypeReference<>() { }; @@ -42,7 +43,7 @@ public class AppVideoHtmlCorrection implements Correction { private final double logSamplingRate; public AppVideoHtmlCorrection(ObjectMapper mapper, double logSamplingRate) { - this.mapper = mapper; + this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; } diff --git a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java index 537e79943cd..9b36b0210da 100644 --- a/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java +++ b/extra/modules/pb-response-correction/src/test/java/org/prebid/server/hooks/modules/pb/response/correction/core/correction/appvideohtml/AppVideoHtmlCorrectionTest.java @@ -77,7 +77,7 @@ public void applyShouldNotChangeBidResponsesWhenBidIsVideoAndHasVastXmlInAdm() { final List givenResponses = List.of( BidderResponse.of("bidderA", null, 100), BidderResponse.of("bidderB", BidderSeatBid.of( - List.of(givenBid("", BidType.video))), 100)); // when final List actual = target.apply(givenConfig, givenResponses); diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy index 589925b3e34..c848c30e2e4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy @@ -37,6 +37,8 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { "adapters.generic.modifying-vast-xml-allowed": "false"] + responseCorrectionConfig) + private final static int OPTIMAL_MAX_LENGTH = 20 + def "PBS shouldn't modify response when in account correction module disabled"() { given: "Start up time" def start = Instant.now() @@ -326,7 +328,7 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { and: "Set bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase("<${PBSUtils.randomString}VAST${PBSUtils.randomString}")) + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) } bidder.setResponse(bidRequest.id, bidResponse) @@ -352,6 +354,69 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { and: "Response shouldn't contain warnings" assert !response.ext.warnings + + where: + admValue << [ + "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}", + "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}>", + "${PBSUtils.randomString}${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST ${PBSUtils.randomString}>", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}>", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}>" + ] + } + + def "PBS should modify response when requested video impression respond with invalid adm VAST keyword"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest) + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + admValue << [ + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST>", + "<${PBSUtils.randomString}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + ] } def "PBS should modify response when requested #mediaType impression respond with adm VAST keyword"() { @@ -365,7 +430,7 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { and: "Set bidder response" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase("<${PBSUtils.randomString}VAST${PBSUtils.randomString}")) + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) } bidder.setResponse(bidRequest.id, bidResponse) @@ -376,7 +441,7 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { when: "PBS processes auction request" def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) - then: "PBS shouldn't emit log" + then: "PBS should emit log" def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) def bidId = bidResponse.seatbid[0].bid[0].id def responseCorrection = getLogsByText(logsByTime, bidId) @@ -401,7 +466,16 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { assert !response.ext.warnings where: - mediaType << [BANNER, AUDIO, NATIVE] + mediaType | admValue + BANNER | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + BANNER | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + BANNER | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + AUDIO | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + AUDIO | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + AUDIO | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" + NATIVE | "${PBSUtils.randomString}<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}" + NATIVE | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + NATIVE | "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}${PBSUtils.randomString}" } def "PBS shouldn't modify response meta.mediaType to video and emit logs when requested impression with video and adm obj with asset"() { From dedd9231a492bea58f7e60bfed3c623cf120734a Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:05:54 +0200 Subject: [PATCH 099/170] ORTB-Blocking Module: Add new configuration options (#3480) --- .../blocking/core/AccountConfigReader.java | 64 ++- .../ortb2/blocking/core/BidsBlocker.java | 28 +- .../ortb2/blocking/core/RequestUpdater.java | 84 ++- .../core/model/BlockedAttributes.java | 3 +- .../core/model/ResponseBlockingConfig.java | 5 +- .../core/AccountConfigReaderTest.java | 285 ++++++++++- .../ortb2/blocking/core/BidsBlockerTest.java | 59 ++- .../blocking/core/RequestUpdaterTest.java | 107 +++- .../ortb2/blocking/core/config/Attribute.java | 12 +- .../functional/model/bidder/BidderName.groovy | 1 + .../model/bidder/GeneralBidderAdapter.groovy | 17 + .../model/bidderspecific/BidderImpExt.groovy | 4 +- .../config/Ortb2BlockingActionOverride.groovy | 18 +- .../config/Ortb2BlockingAttribute.groovy | 14 +- .../Ortb2BlockingAttributeConfig.groovy | 18 +- .../request/auction/BidRequestExt.groovy | 1 + .../model/request/auction/Bidder.groovy | 1 + .../model/request/auction/Imp.groovy | 13 +- .../model/request/auction/ImpExt.groovy | 1 + .../model/request/auction/Ix.groovy | 20 + .../model/request/auction/IxDiag.groovy | 11 + .../model/response/auction/Bid.groovy | 3 +- .../response/auction/BidMediaType.groovy | 18 + .../model/response/auction/ErrorType.groovy | 1 + .../ortb2blocking/Ortb2BlockingSpec.groovy | 477 +++++++++++++----- .../VersionedVendorListServiceTest.java | 2 +- 26 files changed, 1090 insertions(+), 177 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java index 599be6e981c..a7ee0425135 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java @@ -18,6 +18,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.spring.config.bidder.model.MediaType; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -51,7 +52,11 @@ public class AccountConfigReader { private static final String ALLOWED_APP_FOR_DEALS_FIELD = "allowed-app-for-deals"; private static final String BLOCKED_BANNER_TYPE_FIELD = "blocked-banner-type"; private static final String BLOCKED_BANNER_ATTR_FIELD = "blocked-banner-attr"; + private static final String BLOCKED_VIDEO_ATTR_FIELD = "blocked-video-attr"; + private static final String BLOCKED_AUDIO_ATTR_FIELD = "blocked-audio-attr"; private static final String ALLOWED_BANNER_ATTR_FOR_DEALS = "allowed-banner-attr-for-deals"; + private static final String ALLOWED_VIDEO_ATTR_FOR_DEALS = "allowed-video-attr-for-deals"; + private static final String ALLOWED_AUDIO_ATTR_FOR_DEALS = "allowed-audio-attr-for-deals"; private static final String ACTION_OVERRIDES_FIELD = "action-overrides"; private static final String OVERRIDE_FIELD = "override"; private static final String CONDITIONS_FIELD = "conditions"; @@ -100,8 +105,14 @@ public Result blockedAttributesFor(BidRequest bidRequest) { blockedAttribute(BAPP_FIELD, String.class, BLOCKED_APP_FIELD, requestMediaTypes); final Result>> btype = blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, bidRequest); - final Result>> battr = + final Result>> bannerBattr = blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest); + final Result>> videoBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, bidRequest); + final Result>> audioBattr = + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, bidRequest); + final Result>>> battr = + mergeBlockedAttributes(bannerBattr, videoBattr, audioBattr); return Result.of( toBlockedAttributes(badv, bcat, cattaxComplement, bapp, btype, battr), @@ -133,22 +144,39 @@ public Result responseBlockingConfigFor(BidderBid bidder ALLOWED_APP_FOR_DEALS_FIELD, bidMediaTypes, dealid); - final Result> battr = blockingConfigForAttribute( + final Result> bannerBattr = blockingConfigForAttribute( BATTR_FIELD, Integer.class, ALLOWED_BANNER_ATTR_FOR_DEALS, bidMediaTypes, dealid); + final Result> videoBattr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_VIDEO_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + final Result> audioBattr = blockingConfigForAttribute( + BATTR_FIELD, + Integer.class, + ALLOWED_AUDIO_ATTR_FOR_DEALS, + bidMediaTypes, + dealid); + final Map> battr = new HashMap<>(); + battr.put(MediaType.BANNER, bannerBattr.getValue()); + battr.put(MediaType.VIDEO, videoBattr.getValue()); + battr.put(MediaType.AUDIO, audioBattr.getValue()); final ResponseBlockingConfig response = ResponseBlockingConfig.builder() .badv(badv.getValue()) .bcat(bcat.getValue()) .cattax(cattax.getValue()) .bapp(bapp.getValue()) - .battr(battr.getValue()) + .battr(battr) .build(); - final List warnings = MergeUtils.mergeMessages(badv, bcat, cattax, bapp, battr); + final List warnings = MergeUtils.mergeMessages( + badv, bcat, cattax, bapp, bannerBattr, videoBattr, audioBattr); return Result.of(response, warnings); } @@ -218,6 +246,28 @@ private Result>> blockedAttributesForImps(String attribu MergeUtils.mergeMessages(results)); } + private static Result>>> mergeBlockedAttributes( + Result>> bannerBattr, + Result>> videoBattr, + Result>> audioBattr) { + + final Map>> battr = new HashMap<>(); + + if (bannerBattr.hasValue()) { + battr.put(MediaType.BANNER, bannerBattr.getValue()); + } + if (videoBattr.hasValue()) { + battr.put(MediaType.VIDEO, videoBattr.getValue()); + } + if (audioBattr.hasValue()) { + battr.put(MediaType.AUDIO, audioBattr.getValue()); + } + + return Result.of( + !battr.isEmpty() ? battr : null, + MergeUtils.mergeMessages(bannerBattr, videoBattr, audioBattr)); + } + private Result> blockingConfigForAttribute(String attribute, Class attributeType, String blockUnknownField, @@ -360,8 +410,8 @@ private Result toResult(List specificBidderResults, Set actualMediaTypes) { final JsonNode value = ObjectUtils.firstNonNull( - specificBidderResults.size() > 0 ? specificBidderResults.get(0) : null, - catchAllBidderResults.size() > 0 ? catchAllBidderResults.get(0) : null); + !specificBidderResults.isEmpty() ? specificBidderResults.getFirst() : null, + !catchAllBidderResults.isEmpty() ? catchAllBidderResults.getFirst() : null); final List warnings = debugEnabled && specificBidderResults.size() + catchAllBidderResults.size() > 1 ? Collections.singletonList( "More than one conditions matches request. Bidder: %s, request media types: %s" @@ -376,7 +426,7 @@ private static BlockedAttributes toBlockedAttributes(Result> badv, Result cattaxComplement, Result> bapp, Result>> btype, - Result>> battr) { + Result>>> battr) { return badv.hasValue() || bcat.hasValue() diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java index 6e86d4fce0b..2c9b40b6079 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java @@ -18,6 +18,8 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.hooks.modules.ortb2.blocking.core.util.MergeUtils; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.ArrayList; import java.util.Collections; @@ -91,7 +93,6 @@ public ExecutionResult block() { try { final List> blockedBidResults = bids.stream() - .sequential() .map(bid -> isBlocked(bid, accountConfigReader)) .toList(); @@ -170,11 +171,30 @@ private AttributeCheckResult checkBapp(BidderBid bidderBid, ResponseBloc } private AttributeCheckResult checkBattr(BidderBid bidderBid, ResponseBlockingConfig blockingConfig) { - + final MediaType mediaType = mapBidTypeToMediaType(bidderBid.getType()); return checkAttribute( bidderBid.getBid().getAttr(), - blockingConfig.getBattr(), - blockedAttributeValues(BlockedAttributes::getBattr, bidderBid.getBid().getImpid())); + blockingConfig.getBattr().get(mediaType), + blockedAttributeValues( + blockedAttributes -> extractBattrForMediaType(blockedAttributes, mediaType), + bidderBid.getBid().getImpid())); + } + + private static MediaType mapBidTypeToMediaType(BidType bidType) { + return switch (bidType) { + case banner -> MediaType.BANNER; + case video -> MediaType.VIDEO; + case audio -> MediaType.AUDIO; + case xNative -> MediaType.NATIVE; + case null -> null; + }; + } + + private static Map> extractBattrForMediaType(BlockedAttributes blockedAttributes, + MediaType mediaType) { + + final Map>> battr = blockedAttributes.getBattr(); + return battr != null ? battr.get(mediaType) : null; } private AttributeCheckResult checkAttribute(List attribute, diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java index f83d8554a2c..ac963b94857 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java @@ -1,15 +1,19 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; public class RequestUpdater { @@ -40,39 +44,93 @@ public BidRequest update(BidRequest bidRequest) { private List updateImps(List imps) { final Map> blockedBannerType = blockedAttributes.getBtype(); - final Map> blockedBannerAttr = blockedAttributes.getBattr(); + final Map>> blockedAttr = blockedAttributes.getBattr(); - if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedBannerAttr)) { + if (MapUtils.isEmpty(blockedBannerType) && MapUtils.isEmpty(blockedAttr)) { return imps; } return imps.stream() - .map(imp -> updateImp(imp, blockedBannerType, blockedBannerAttr)) + .map(imp -> updateImp(imp, blockedBannerType, blockedAttr)) .toList(); } private Imp updateImp(Imp imp, Map> blockedBannerType, - Map> blockedBannerAttr) { + Map>> blockedAttr) { final String impId = imp.getId(); final List btypeForImp = blockedBannerType != null ? blockedBannerType.get(impId) : null; - final List battrForImp = blockedBannerAttr != null ? blockedBannerAttr.get(impId) : null; + final List bannerBattrForImp = extractBattr(blockedAttr, MediaType.BANNER, impId); + final List videoBattrForImp = extractBattr(blockedAttr, MediaType.VIDEO, impId); + final List audioBattrForImp = extractBattr(blockedAttr, MediaType.AUDIO, impId); + + if (CollectionUtils.isEmpty(btypeForImp) + && CollectionUtils.isEmpty(bannerBattrForImp) + && CollectionUtils.isEmpty(videoBattrForImp) + && CollectionUtils.isEmpty(audioBattrForImp)) { - if (CollectionUtils.isEmpty(btypeForImp) && CollectionUtils.isEmpty(battrForImp)) { return imp; } final Banner banner = imp.getBanner(); - final List existingBtype = banner != null ? banner.getBtype() : null; - final List existingBattr = banner != null ? banner.getBattr() : null; - final Banner.BannerBuilder bannerBuilder = banner != null ? banner.toBuilder() : Banner.builder(); + final Video video = imp.getVideo(); + final Audio audio = imp.getAudio(); return imp.toBuilder() - .banner(bannerBuilder - .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btypeForImp) - .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battrForImp) - .build()) + .banner(CollectionUtils.isNotEmpty(btypeForImp) || CollectionUtils.isNotEmpty(bannerBattrForImp) + ? updateBanner(banner, btypeForImp, bannerBattrForImp) + : banner) + .video(CollectionUtils.isNotEmpty(videoBattrForImp) + ? updateVideo(imp.getVideo(), videoBattrForImp) + : video) + .audio(CollectionUtils.isNotEmpty(audioBattrForImp) + ? updateAudio(imp.getAudio(), audioBattrForImp) + : audio) .build(); } + + private static List extractBattr(Map>> blockedAttr, + MediaType mediaType, + String impId) { + + final Map> impIdToBattr = blockedAttr != null ? blockedAttr.get(mediaType) : null; + return impIdToBattr != null ? impIdToBattr.get(impId) : null; + } + + private static Banner updateBanner(Banner banner, List btype, List battr) { + final List existingBtype = banner != null ? banner.getBtype() : null; + final List existingBattr = banner != null ? banner.getBattr() : null; + + return CollectionUtils.isEmpty(existingBtype) || CollectionUtils.isEmpty(existingBattr) + ? Optional.ofNullable(banner) + .map(Banner::toBuilder) + .orElseGet(Banner::builder) + .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btype) + .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battr) + .build() + : banner; + } + + private static Video updateVideo(Video video, List battr) { + final List existingBattr = video != null ? video.getBattr() : null; + return CollectionUtils.isEmpty(existingBattr) + ? Optional.ofNullable(video) + .map(Video::toBuilder) + .orElseGet(Video::builder) + .battr(battr) + .build() + : video; + } + + private static Audio updateAudio(Audio audio, List battr) { + final List existingBattr = audio != null ? audio.getBattr() : null; + return CollectionUtils.isEmpty(existingBattr) + ? Optional.ofNullable(audio) + .map(Audio::toBuilder) + .orElseGet(Audio::builder) + .battr(battr) + .build() + : audio; + } } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java index d3d3049b57c..aad04ba8db6 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/BlockedAttributes.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; import java.util.Map; @@ -20,5 +21,5 @@ public class BlockedAttributes { Map> btype; - Map> battr; + Map>> battr; } diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java index c2108eb8a8f..8c34561079e 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ResponseBlockingConfig.java @@ -2,6 +2,9 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.spring.config.bidder.model.MediaType; + +import java.util.Map; @Builder @Value @@ -15,5 +18,5 @@ public class ResponseBlockingConfig { BidAttributeBlockingConfig bapp; - BidAttributeBlockingConfig battr; + Map> battr; } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java index 456ac49939a..f6568663807 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java @@ -30,6 +30,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ResponseBlockingConfig; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.Result; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.HashMap; import java.util.HashSet; @@ -734,7 +735,7 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri } @Test - public void blockedAttributesForShouldReturnResultWithAllAttributes() { + public void blockedAttributesForShouldReturnResultWithAllAttributesForBanner() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -766,7 +767,7 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { Conditions.of(singletonList("bidder1"), null), singletonList(3))))) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .blocked(asList(1, 2)) .actionOverrides(AttributeActionOverrides.blocked(singletonList( ArrayOverride.of( @@ -783,7 +784,115 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { .bcat(singletonList("cat3")) .bapp(singletonList("app3")) .btype(singletonMap("impId1", singletonList(3))) - .battr(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", singletonList(3)))) + .build())); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributesForVideo() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.videoBattrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( + Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .btype(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", singletonList(3)))) + .build())); + } + + @Test + public void blockedAttributesForShouldReturnResultWithAllAttributesForAudio() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .blocked(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.blocked( + singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .blocked(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .blocked(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList("app3"))))) + .build()) + .btype(Attribute.btypeBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .battr(Attribute.audioBattrBuilder() + .blocked(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.blocked(singletonList( + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), null), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( + Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .btype(singletonMap("impId1", singletonList(3))) + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", singletonList(3)))) .build())); } @@ -1143,7 +1252,163 @@ public void responseBlockingConfigForShouldReturnResultWithMergedDealExceptionsW } @Test - public void responseBlockingConfigForShouldReturnAllAttributes() { + public void responseBlockingConfigForShouldReturnAllAttributesForBanner() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.bannerBattrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, Set.of("domain1.com", "domain2.com", "domain3.com"))) + .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) + .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) + .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, emptySet()))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributesForVideo() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .badv(Attribute.badvBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("domain1.com", "domain2.com")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("domain3.com"))))) + .build()) + .bcat(Attribute.bcatBuilder() + .enforceBlocks(true) + .blockUnknown(true) + .allowedForDeals(asList("cat1", "cat2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("cat3"))))) + .build()) + .bapp(Attribute.bappBuilder() + .enforceBlocks(true) + .allowedForDeals(asList("app1", "app2")) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList("app3"))))) + .build()) + .battr(Attribute.videoBattrBuilder() + .enforceBlocks(true) + .allowedForDeals(asList(1, 2)) + .actionOverrides(AttributeActionOverrides.response( + singletonList(BooleanOverride.of( + Conditions.of(singletonList("bidder1"), null), + false)), + null, + singletonList(AllowedForDealsOverride.of( + DealsConditions.of(singletonList("dealid1")), + singletonList(3))))) + .build()) + .build())); + final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); + + // when and then + assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { + assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() + .badv(BidAttributeBlockingConfig.of( + false, false, Set.of("domain1.com", "domain2.com", "domain3.com"))) + .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) + .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) + .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, emptySet()))) + .build()); + assertThat(result.getMessages()).isNull(); + }); + } + + @Test + public void responseBlockingConfigForShouldReturnAllAttributesForAudio() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -1188,7 +1453,7 @@ public void responseBlockingConfigForShouldReturnAllAttributes() { DealsConditions.of(singletonList("dealid1")), singletonList("app3"))))) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.audioBattrBuilder() .enforceBlocks(true) .allowedForDeals(asList(1, 2)) .actionOverrides(AttributeActionOverrides.response( @@ -1211,7 +1476,10 @@ public void responseBlockingConfigForShouldReturnAllAttributes() { .bcat(BidAttributeBlockingConfig.of(false, false, Set.of("cat1", "cat2", "cat3"))) .cattax(BidAttributeBlockingConfig.of(false, true, emptySet())) .bapp(BidAttributeBlockingConfig.of(false, false, Set.of("app1", "app2", "app3"))) - .battr(BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3))) + .battr(Map.of( + MediaType.BANNER, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.VIDEO, BidAttributeBlockingConfig.of(false, false, emptySet()), + MediaType.AUDIO, BidAttributeBlockingConfig.of(false, false, Set.of(1, 2, 3)))) .build()); assertThat(result.getMessages()).isNull(); }); @@ -1240,10 +1508,15 @@ public void responseBlockingConfigForShouldReturnCattaxConfigDependsOnBcatConfig final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then + final Map> expectedBattr = new HashMap<>(); + expectedBattr.put(MediaType.BANNER, null); + expectedBattr.put(MediaType.VIDEO, null); + expectedBattr.put(MediaType.AUDIO, null); assertThat(reader.responseBlockingConfigFor(bid())).satisfies(result -> { assertThat(result.getValue()).isEqualTo(ResponseBlockingConfig.builder() .bcat(BidAttributeBlockingConfig.of(true, false, Set.of("cat1", "cat2", "cat3"))) .cattax(BidAttributeBlockingConfig.of(true, true, emptySet())) + .battr(expectedBattr) .build()); assertThat(result.getMessages()).isNull(); }); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java index a995b43c90a..79b1309a2ba 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java @@ -21,6 +21,7 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.HashMap; import java.util.List; @@ -227,7 +228,7 @@ public void shouldReturnEmptyResultWhenBidWithAdomainAndNoBlockedAttributes() { public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); @@ -237,7 +238,53 @@ public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() .impid("impId2") .attr(singletonList(1)))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() - .battr(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedVideoAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.videoBattrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + verifyNoInteractions(bidRejectionTracker); + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedAudioAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.audioBattrBuilder() + .enforceBlocks(true) + .build()) + .build())); + + // when + final List bids = singletonList(bid(bid -> bid + .impid("impId2") + .attr(singletonList(1)))); + final BlockedAttributes blockedAttributes = BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", asList(1, 2)))) .build(); final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); @@ -341,7 +388,7 @@ public void shouldReturnResultWithAnalyticsResults() { .bapp(Attribute.bappBuilder() .enforceBlocks(true) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); @@ -366,7 +413,7 @@ public void shouldReturnResultWithAnalyticsResults() { .badv(asList("domain1.com", "domain2.com", "domain3.com")) .bcat(asList("cat1", "cat2", "cat3")) .bapp(asList("app1", "app2", "app3")) - .battr(singletonMap("impId2", asList(1, 2, 3))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId2", asList(1, 2, 3)))) .build(); final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); @@ -413,7 +460,7 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .enforceBlocks(true) .allowedForDeals(singletonList("app2")) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .allowedForDeals(singletonList(2)) .build()) @@ -441,7 +488,7 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .badv(asList("domain1.com", "domain2.com")) .bcat(asList("cat1", "cat2")) .bapp(asList("app1", "app2")) - .battr(singletonMap("impId1", asList(1, 2))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) .build(); final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true); diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java index 39b5807ab12..630c09e96d1 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java @@ -1,12 +1,16 @@ package org.prebid.server.hooks.modules.ortb2.blocking.core; +import com.iab.openrtb.request.Audio; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; +import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.List; +import java.util.Map; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -138,10 +142,12 @@ public void shouldReplaceImpBtypeWhenAbsent() { } @Test - public void shouldReplaceImpBattrWhenAbsent() { + public void shouldReplaceImpBannerBattrWhenAbsent() { // given final RequestUpdater updater = RequestUpdater.create( - BlockedAttributes.builder().battr(singletonMap("impId1", asList(1, 2))).build()); + BlockedAttributes.builder() + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", asList(1, 2)))) + .build()); final BidRequest request = BidRequest.builder() .imp(singletonList(Imp.builder() .id("impId1") @@ -160,6 +166,56 @@ public void shouldReplaceImpBattrWhenAbsent() { .build()); } + @Test + public void shouldReplaceImpVideoBattrWhenAbsent() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", asList(1, 2)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + + @Test + public void shouldReplaceImpAudioBattrWhenAbsent() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", asList(1, 2)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .audio(Audio.builder() + .battr(asList(1, 2)) + .build()) + .build())) + .build()); + } + @Test public void shouldNotChangeImpsWhenNoBlockedBannerTypeAndBlockedBannerAttr() { // given @@ -180,7 +236,43 @@ public void shouldNotChangeImpWhenNoBlockedBannerTypeAndBlockedBannerAttrForImp( final RequestUpdater updater = RequestUpdater.create( BlockedAttributes.builder() .btype(singletonMap("impId2", singletonList(1))) - .battr(singletonMap("impId2", singletonList(1))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId2", singletonList(1)))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedVideoAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.VIDEO, singletonMap("impId2", singletonList(1)))) + .build()); + final Imp imp = Imp.builder().build(); + final BidRequest request = BidRequest.builder() + .imp(singletonList(imp)) + .build(); + + // when and then + final BidRequest updatedRequest = updater.update(request); + assertThat(updatedRequest.getImp()).hasSize(1); + assertThat(updatedRequest.getImp().get(0)).isSameAs(imp); + } + + @Test + public void shouldNotChangeImpWhenNoBlockedAudioAttrForImp() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId2", singletonList(1)))) .build()); final Imp imp = Imp.builder().build(); final BidRequest request = BidRequest.builder() @@ -198,7 +290,7 @@ public void shouldKeepImpBtypeWhenNoBlockedBannerTypeAndPresentBlockedBannerAttr // given final RequestUpdater updater = RequestUpdater.create( BlockedAttributes.builder() - .battr(singletonMap("impId1", singletonList(1))) + .battr(singletonMap(MediaType.BANNER, singletonMap("impId1", singletonList(1)))) .build()); final Imp imp = Imp.builder() .id("impId1") @@ -256,7 +348,10 @@ public void shouldUpdateAllAttributes() { .bcat(asList("cat1", "cat2")) .bapp(asList("app1", "app2")) .btype(singletonMap("impId1", asList(1, 2))) - .battr(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) .build()); final BidRequest request = BidRequest.builder() .imp(singletonList(Imp.builder().id("impId1").build())) @@ -273,6 +368,8 @@ public void shouldUpdateAllAttributes() { .btype(asList(1, 2)) .battr(asList(1, 2)) .build()) + .video(Video.builder().battr(asList(3, 4)).build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) .build())) .build()); } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java index 6e7d3257a33..e60354670e9 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/Attribute.java @@ -63,8 +63,18 @@ public static AttributeBuilder btypeBuilder() { .field("banner-type"); } - public static AttributeBuilder battrBuilder() { + public static AttributeBuilder bannerBattrBuilder() { return Attribute.builder() .field("banner-attr"); } + + public static AttributeBuilder videoBattrBuilder() { + return Attribute.builder() + .field("video-attr"); + } + + public static AttributeBuilder audioBattrBuilder() { + return Attribute.builder() + .field("audio-attr"); + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy index 1502df5ed70..f91f209395c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy @@ -21,6 +21,7 @@ enum BidderName { ACUITYADS("acuityads"), AAX("aax"), ADKERNEL("adkernel"), + IX("ix"), GRID("grid"), MEDIANET("medianet") diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy new file mode 100644 index 00000000000..dbd47dcda4d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.bidder + +import com.fasterxml.jackson.annotation.JsonProperty + +class GeneralBidderAdapter implements BidderAdapter { + + Object exampleProperty + Integer firstParam + Integer secondParam + @JsonProperty("dealsonly") + Boolean dealsOnly + @JsonProperty("pgdealsonly") + Boolean pgDealsOnly + String siteId + List size + String sid +} diff --git a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy index e0e3b26d02a..d07ca31ad9d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidderspecific/BidderImpExt.groovy @@ -1,12 +1,12 @@ package org.prebid.server.functional.model.bidderspecific import groovy.transform.ToString -import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.GeneralBidderAdapter import org.prebid.server.functional.model.request.auction.ImpExt @ToString(includeNames = true, ignoreNulls = true) class BidderImpExt extends ImpExt { - Generic bidder + GeneralBidderAdapter bidder Rp rp } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy index 6e09273bef7..de8eae06d04 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingActionOverride.groovy @@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP -import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) @@ -18,6 +20,8 @@ class Ortb2BlockingActionOverride { List blockedAdomain List blockedApp List blockedBannerAttr + List blockedVideoAttr + List blockedAudioAttr List blockedAdvCat List blockedBannerType @@ -27,6 +31,8 @@ class Ortb2BlockingActionOverride { List allowedAdomainForDeals List allowedAppForDeals List allowedBannerAttrForDeals + List allowedVideoAttrForDeals + List allowedAudioAttrForDeals List allowedAdvCatForDeals static Ortb2BlockingActionOverride getDefaultOverride(Ortb2BlockingAttribute attribute, @@ -43,10 +49,18 @@ class Ortb2BlockingActionOverride { blockedApp = blocked allowedAppForDeals = allowedForDeals break - case BATTR: + case BANNER_BATTR: blockedBannerAttr = blocked allowedBannerAttrForDeals = allowedForDeals break + case VIDEO_BATTR: + blockedVideoAttr = blocked + allowedVideoAttrForDeals = allowedForDeals + break + case AUDIO_BATTR: + blockedAudioAttr = blocked + allowedAudioAttrForDeals = allowedForDeals + break case BCAT: blockedAdvCat = blocked allowedAdvCatForDeals = allowedForDeals diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy index e1688e2d2b3..15c54c2c021 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttribute.groovy @@ -6,10 +6,18 @@ import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) enum Ortb2BlockingAttribute { - BADV, BAPP, BATTR, BCAT, BTYPE + BADV('badv'), + BAPP('bapp'), + BANNER_BATTR('battr'), + VIDEO_BATTR('battr'), + AUDIO_BATTR('battr'), + BCAT('bcat'), + BTYPE('btype') @JsonValue - String getValue() { - name().toLowerCase() + final String value + + Ortb2BlockingAttribute(String value) { + this.value = value } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy index 5c405269466..9e622472024 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Ortb2BlockingAttributeConfig.groovy @@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP -import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) @@ -19,6 +21,8 @@ class Ortb2BlockingAttributeConfig { Object blockedAdomain Object blockedApp Object blockedBannerAttr + Object blockedVideoAttr + Object blockedAudioAttr Object blockedAdvCat Object blockedBannerType @@ -28,6 +32,8 @@ class Ortb2BlockingAttributeConfig { Object allowedAdomainForDeals Object allowedAppForDeals Object allowedBannerAttrForDeals + Object allowedVideoAttrForDeals + Object allowedAudioAttrForDeals Object allowedAdvCatForDeals Ortb2BlockingActionOverride actionOverrides @@ -44,10 +50,18 @@ class Ortb2BlockingAttributeConfig { blockedApp = ortb2Attributes allowedAppForDeals = ortb2AttributesForDeals break - case BATTR: + case BANNER_BATTR: blockedBannerAttr = ortb2Attributes allowedBannerAttrForDeals = ortb2AttributesForDeals break + case VIDEO_BATTR: + blockedVideoAttr = ortb2Attributes + allowedVideoAttrForDeals = ortb2AttributesForDeals + break + case AUDIO_BATTR: + blockedAudioAttr = ortb2Attributes + allowedAudioAttrForDeals = ortb2AttributesForDeals + break case BCAT: blockedAdvCat = ortb2Attributes allowedAdvCatForDeals = ortb2AttributesForDeals diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy index f7aee45fb75..c253291dbf8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequestExt.groovy @@ -11,4 +11,5 @@ class BidRequestExt { AppNexus appnexus String bc String platform + IxDiag ixdiag } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy index 9b5c78f5d97..a1078731f44 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy @@ -19,6 +19,7 @@ class Bidder { @JsonProperty("appnexus") AppNexus appNexus Openx openx + Ix ix static Bidder getDefaultBidder() { new Bidder().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index 8ea26570de9..dbea9b32624 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -90,7 +90,8 @@ class Imp { (bidder.genericCamelCase): BidderName.GENERIC_CAMEL_CASE, (bidder.rubicon) : BidderName.RUBICON, (bidder.appNexus) : BidderName.APPNEXUS, - (bidder.openx) : BidderName.OPENX + (bidder.openx) : BidderName.OPENX, + (bidder.ix) : BidderName.IX ].findAll { it.key } if (bidderNames.size() != 1) { @@ -99,4 +100,14 @@ class Imp { bidderNames.values().first() } + + @JsonIgnore + List getMediaTypes() { + return [ + (banner ? BANNER : null), + (video ? VIDEO : null), + (nativeObj ? NATIVE : null), + (audio ? AUDIO : null) + ].findAll { it } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy index fe8cd0f089c..e817a4540a0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpExt.groovy @@ -22,6 +22,7 @@ class ImpExt { ImpExtContextData data String tid String gpid + String sid Integer ae String all String skadn diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy new file mode 100644 index 00000000000..a620c7646b1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Ix.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.EqualsAndHashCode +import org.prebid.server.functional.util.PBSUtils + +@EqualsAndHashCode +class Ix { + + String siteId + List size + String sid + + static Ix getDefault() { + new Ix().tap { + siteId = PBSUtils.randomString + size = [PBSUtils.randomNumber, PBSUtils.randomNumber] + sid = PBSUtils.randomString + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy new file mode 100644 index 00000000000..7bc0adc1054 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class IxDiag { + + String pbsv + String pbjsv + String multipleSiteIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index e4fb04de375..22e29b76908 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -47,7 +47,8 @@ class Bid implements ObjectMapperWrapper { Integer heightRatio Integer exp Integer dur - Integer mtype + @JsonProperty("mtype") + BidMediaType mediaType Integer slotinpod BidExt ext diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy new file mode 100644 index 00000000000..76aa2a558f9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum BidMediaType { + + BANNER(1), + VIDEO(2), + AUDIO(3), + NATIVE(4) + + @JsonValue + final Integer value + + BidMediaType(Integer value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy index e62b548fbe6..267a23cc067 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ErrorType.groovy @@ -13,6 +13,7 @@ enum ErrorType { CACHE("cache"), ALIAS("alias"), TARGETING("targeting"), + IX("ix"), OPENX("openx") @JsonValue diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy index 4df794b3ead..b37cae6a067 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -2,7 +2,6 @@ package org.prebid.server.functional.tests.module.ortb2blocking import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.bidder.Generic -import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration import org.prebid.server.functional.model.config.ExecutionPlan @@ -14,9 +13,16 @@ import org.prebid.server.functional.model.config.Ortb2BlockingConfig import org.prebid.server.functional.model.config.Ortb2BlockingOverride import org.prebid.server.functional.model.config.PbsModulesConfig import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.Asset +import org.prebid.server.functional.model.request.auction.Audio +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.Ix import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.Video +import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidMediaType import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.model.response.auction.MediaType @@ -24,36 +30,39 @@ import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.tests.module.ModuleBaseSpec import org.prebid.server.functional.util.PBSUtils -import spock.lang.PendingFeature import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC -import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.IX import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.AUDIO_BATTR import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BADV import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BAPP -import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BATTR +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BANNER_BATTR import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BCAT import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.BTYPE +import static org.prebid.server.functional.model.config.Ortb2BlockingAttribute.VIDEO_BATTR import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO import static org.prebid.server.functional.model.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class Ortb2BlockingSpec extends ModuleBaseSpec { - private static final Map OPENX_CONFIG = ["adapters.openx.enabled" : "true", - "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final Map IX_CONFIG = ["adapters.ix.enabled" : "true", + "adapters.ix.endpoint": "$networkServiceContainer.rootUri/auction".toString()] private static final String WILDCARD = '*' - private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + OPENX_CONFIG) + private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + IX_CONFIG + + ["adapters.generic.ortb.multiformat-supported": "true"]) def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) @@ -83,13 +92,15 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR PBSUtils.randomNumber | BTYPE } def "PBS should be able to send original array ortb2 attribute to bidder alias"() { given: "Default bid request with alias" - def bidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { ext.prebid.aliases = [(ALIAS.value): GENERIC] imp[0].ext.prebid.bidder.generic = null imp[0].ext.prebid.bidder.alias = new Generic() @@ -114,13 +125,15 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR PBSUtils.randomNumber | BTYPE } def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, ortb2Attributes, attributeName) @@ -151,13 +164,15 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR PBSUtils.randomNumber | BTYPE } def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) @@ -182,13 +197,15 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomNumber | BADV PBSUtils.randomNumber | BAPP PBSUtils.randomNumber | BCAT - PBSUtils.randomString | BATTR + PBSUtils.randomString | BANNER_BATTR + PBSUtils.randomString | VIDEO_BATTR + PBSUtils.randomString | AUDIO_BATTR PBSUtils.randomString | BTYPE } def "PBS shouldn't send original inappropriate ortb2 attribute to bidder when blocking is enabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { @@ -220,12 +237,14 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should send only not matched ortb2 attribute to bidder when blocking is enabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { @@ -255,16 +274,82 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert !response?.ext?.prebid?.modules?.warnings where: - allowedOrtb2Attributes | disallowedOrtb2Attributes | attributeName - PBSUtils.randomString | PBSUtils.randomString | BADV - PBSUtils.randomString | PBSUtils.randomString | BAPP - PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + allowedOrtb2Attributes | disallowedOrtb2Attributes | attributeName + PBSUtils.randomString | PBSUtils.randomString | BADV + PBSUtils.randomString | PBSUtils.randomString | BAPP + PBSUtils.randomString | PBSUtils.randomString | BCAT + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | BANNER_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | VIDEO_BATTR + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber | AUDIO_BATTR + } + + def "PBS should left only not matched ortb2 attribute to bidder with multiply type imp when blocking is enabled"() { + given: "Default bid request with proper ortb attribute" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + banner = Banner.getDefaultBanner().tap { + battr = [PBSUtils.randomNumber] + } + video = Video.getDefaultVideo().tap { + battr = [PBSUtils.randomNumber] + } + audio = Audio.getDefaultAudio().tap { + battr = [PBSUtils.randomNumber] + } + ext.prebid.bidder.generic = null + ext.prebid.bidder.ix = Ix.default + } + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def disallowedOrtb2Attributes = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([disallowedOrtb2Attributes], attributeName).tap { + enforceBlocks = true + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def removeBid = getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName).tap { + it.mediaType = enforceType + } + def presentBid = getBidWithOrtb2Attribute(bidRequest.imp.first, disallowedOrtb2Attributes, attributeName).tap { + it.mediaType = presentType + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [removeBid, presentBid] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response should contain only allowed seatbid" + assert response.seatbid.bid.flatten().size() == 1 + assert response.seatbid.first.bid.first.mediaType == presentType + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [disallowedOrtb2Attributes]*.toString() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + attributeName | enforceType | presentType + BANNER_BATTR | BidMediaType.BANNER | BidMediaType.AUDIO + VIDEO_BATTR | BidMediaType.VIDEO | BidMediaType.BANNER + AUDIO_BATTR | BidMediaType.AUDIO | BidMediaType.VIDEO } def "PBS should send original inappropriate ortb2 attribute to bidder when blocking is disabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { @@ -296,12 +381,14 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should discard unknown adomain bids when enforcement is enabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true) @@ -339,8 +426,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should not discard unknown adomain bids when enforcement is disabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) and: "Account in the DB with blocking configuration" def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BADV): ortb2BlockingAttributeConfig]) @@ -374,8 +461,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should discard unknown adv cat bids when enforcement is enabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true) @@ -413,8 +500,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should not discard unknown adv cat bids when enforcement is disabled"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) and: "Account in the DB with blocking configuration" def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BCAT): ortb2BlockingAttributeConfig]) @@ -448,8 +535,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should not discard bids with deals when allowed ortb2 attribute for deals is matched"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def attributes = [(attributeName): Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName, [ortb2Attributes]).tap { @@ -483,12 +570,14 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should discard bids with deals when allowed ortb2 attribute for deals is not matched"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def attributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([allowedOrtb2Attributes, dielsOrtb2Attributes], attributeName, [allowedOrtb2Attributes]).tap { @@ -521,17 +610,19 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should be able to override enforcement by bidder"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { + imp[0].ext.prebid.bidder.ix = Ix.default } and: "Account in the DB with blocking configuration" - def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { enforceBlocks = true actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) @@ -542,7 +633,7 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { and: "Default bidder response with ortb2 attributes" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: GENERIC), - new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: OPENX)] + new SeatBid(bid: [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attributes, attributeName)], seat: IX)] } bidder.setResponse(bidRequest.id, bidResponse) @@ -551,7 +642,7 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { then: "PBS response should contain only openx seatbid" assert response.seatbid.size() == 1 - assert response.seatbid.first.seat == OPENX + assert response.seatbid.first.seat == IX assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attributes]*.toString() and: "PBS response shouldn't contain any module errors" @@ -565,14 +656,16 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | BADV PBSUtils.randomString | BAPP PBSUtils.randomString | BCAT - PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should be able to override enforcement by media type"() { - given: "Default bidRequest" + given: "Bid request with multy type imp" def bannerImp = Imp.getDefaultImpression(BANNER) def videoImp = Imp.getDefaultImpression(VIDEO) - def bidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = getBidRequestForOrtbAttribute(attributeName).tap { imp = [bannerImp, videoImp] } @@ -614,25 +707,31 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should be able to override enforcement by media type for battr attribute"() { - given: "Default bidRequest" - def bannerImp = Imp.getDefaultImpression(BANNER) - def bidRequest = BidRequest.defaultBidRequest.tap { - imp = [bannerImp] + given: "Default bid request with proper ortb attribute" + BidRequest bidRequest = getBidRequestForOrtbAttribute(attributeName, [PBSUtils.randomNumber]).tap { +// default resolve for bids always prefer type from request, ix from response and only then from request if null + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default } and: "Account in the DB with blocking configuration" - def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) + def blockingCondition = new Ortb2BlockingConditions(mediaType: [mediaType]) def ortb2Attribute = PBSUtils.randomNumber - def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], BATTR).tap { + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], attributeName).tap { enforceBlocks = true actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) } - def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(BATTR): ortb2BlockingAttributeConfig]) + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) accountDao.save(account) and: "Default bidder response with ortb2 attributes" + def bid = getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName).tap { + it.mediaType = bidMediaType + it.adm = new Adm(assets: [Asset.defaultAsset]) // required for video type + } def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - it.seatbid = [new SeatBid(bid: [getBidWithOrtb2Attribute(bannerImp, ortb2Attribute, BATTR)])] + it.seatbid = [new SeatBid(bid: [bid])] + } bidder.setResponse(bidRequest.id, bidResponse) @@ -641,19 +740,81 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { then: "PBS response should contain banner seatbid" assert response.seatbid.bid.flatten().size() == 1 - assert response.seatbid.first.bid.first.impid == bannerImp.id - assert getOrtb2Attributes(response.seatbid.first.bid.first, BATTR) == [ortb2Attribute]*.toString() + assert response.seatbid.first.bid.first.impid == bidRequest.imp.first.id + assert getOrtb2Attributes(response.seatbid.first.bid.first, attributeName) == [ortb2Attribute]*.toString() and: "PBS response shouldn't contain any module errors" assert !response?.ext?.prebid?.modules?.errors and: "PBS response shouldn't contain any module warning" assert !response?.ext?.prebid?.modules?.warnings + + where: + attributeName | mediaType | bidMediaType + BANNER_BATTR | BANNER | null + VIDEO_BATTR | VIDEO | null + AUDIO_BATTR | AUDIO | null + BANNER_BATTR | BANNER | BidMediaType.BANNER + VIDEO_BATTR | VIDEO | BidMediaType.VIDEO + AUDIO_BATTR | AUDIO | BidMediaType.AUDIO + BANNER_BATTR | BANNER | BidMediaType.AUDIO + VIDEO_BATTR | VIDEO | BidMediaType.BANNER + AUDIO_BATTR | AUDIO | BidMediaType.VIDEO + } + + def "PBS shouldn't be able to override enforcement by incorrect media type for battr attribute"() { + given: "Default bid request with proper ortb attribute" + BidRequest bidRequest = getBidRequestForOrtbAttribute(attributeName, [PBSUtils.randomNumber]).tap { + // default resolve for bids always prefer type from request, ix from response and only then from request if null + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.ix = Ix.default + } + + and: "Account in the DB with blocking configuration" + def blockingCondition = new Ortb2BlockingConditions(mediaType: [mediaType]) + def ortb2Attribute = PBSUtils.randomNumber + def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attribute], attributeName).tap { + enforceBlocks = true + actionOverrides = new Ortb2BlockingActionOverride(enforceBlocks: [new Ortb2BlockingOverride(override: false, conditions: blockingCondition)]) + } + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [(attributeName): ortb2BlockingAttributeConfig]) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bid = getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName).tap { + it.mediaType = bidMediaType + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [bid])] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain any seatbid" + assert !response.seatbid.bid.flatten().size() + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + and: "PBS request should contain original ortb2 attribute" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert getOrtb2Attributes(bidderRequest, attributeName) == getOrtb2Attributes(bidRequest, attributeName) + + where: + attributeName | mediaType | bidMediaType + BANNER_BATTR | AUDIO | null + VIDEO_BATTR | BANNER | null + AUDIO_BATTR | VIDEO | null } def "PBS should be able to override enforcement by deal id"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def blockingCondition = new Ortb2BlockingOverride(override: [ortb2Attributes], conditions: new Ortb2BlockingConditions(dealIds: [dealId.toString()])) @@ -689,16 +850,20 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomNumber | PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BADV WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BAPP WILDCARD | PBSUtils.randomString | PBSUtils.randomString | BCAT - WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + WILDCARD | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should be able to override blocked ortb2 attribute by bidder"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def blockingCondition = new Ortb2BlockingConditions(bidders: [GENERIC]) @@ -734,12 +899,14 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should be able to override blocked ortb2 attribute by media type"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) @@ -775,17 +942,19 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR } def "PBS should be able to override block unknown adomain by bidder"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV).tap { + imp[0].ext.prebid.bidder.ix = Ix.default } and: "Account in the DB with blocking configuration" - def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdomain: true).tap { @@ -800,16 +969,16 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { it.seatbid = [new SeatBid(bid: [bidWithOutAdomain], seat: GENERIC), - new SeatBid(bid: [bidWithOutAdomain], seat: OPENX)] + new SeatBid(bid: [bidWithOutAdomain], seat: IX)] } bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes the auction request" def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) - then: "PBS response should contain only openx seatbid" + then: "PBS response should contain only ix seatbid" assert response.seatbid.bid.flatten().size() == 1 - assert response.seatbid.first.seat == OPENX + assert response.seatbid.first.seat == IX and: "PBS response shouldn't contain any module errors" assert !response?.ext?.prebid?.modules?.errors @@ -819,8 +988,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should be able to override block unknown adomain by media type"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BADV) and: "Account in the DB with blocking configuration" def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) @@ -855,13 +1024,13 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should be able to override block unknown adv-cat by bidder"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT).tap { + imp[0].ext.prebid.bidder.ix = Ix.default } and: "Account in the DB with blocking configuration" - def blockingCondition = new Ortb2BlockingConditions(bidders: [OPENX]) + def blockingCondition = new Ortb2BlockingConditions(bidders: [IX]) and: "Account in the DB with blocking configuration" def ortb2BlockingAttributeConfig = new Ortb2BlockingAttributeConfig(enforceBlocks: true, blockUnknownAdvCat: true).tap { @@ -876,16 +1045,16 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { it.seatbid = [new SeatBid(bid: [bidWithOutCat], seat: GENERIC), - new SeatBid(bid: [bidWithOutCat], seat: OPENX)] + new SeatBid(bid: [bidWithOutCat], seat: IX)] } bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes the auction request" def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) - then: "PBS response should contain only openx seatbid" + then: "PBS response should contain only ix seatbid" assert response.seatbid.bid.flatten().size() == 1 - assert response.seatbid.first.seat == OPENX + assert response.seatbid.first.seat == IX and: "PBS response shouldn't contain any module errors" assert !response?.ext?.prebid?.modules?.errors @@ -895,8 +1064,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should be able to override block unknown adv-cat by media type"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(BCAT) and: "Account in the DB with blocking configuration" def blockingCondition = new Ortb2BlockingConditions(mediaType: [BANNER]) @@ -929,8 +1098,8 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { } def "PBS should be able to override allowed ortb2 attribute for deals by deal ids"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def dealId = PBSUtils.randomNumber @@ -968,12 +1137,14 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should use first override when multiple match same condition"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: blockingCondition) @@ -1003,23 +1174,27 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { and: "PBS response should contain proper warning" assert response?.ext?.prebid?.modules?.warnings?.ortb2Blocking["ortb2-blocking-bidder-request"] == - ["More than one conditions matches request. Bidder: generic, request media types: [banner]"] + ["More than one conditions matches request. Bidder: generic, request media types: [${bidRequest.imp[0].mediaTypes[0].value}]"] where: blockingCondition | ortb2Attributes | firstOverrideAttributes | secondOverrideAttributes | attributeName new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT - new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + new Ortb2BlockingConditions(bidders: [GENERIC]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT - new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + new Ortb2BlockingConditions(mediaType: [BANNER]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + new Ortb2BlockingConditions(mediaType: [VIDEO]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + new Ortb2BlockingConditions(mediaType: [AUDIO]) | PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should prefer non wildcard override when multiple match same condition by bidder"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(bidders: [BidderName.WILDCARD])) @@ -1055,16 +1230,18 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should prefer non wildcard override when multiple match same condition by media type"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def firstOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [firstOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [MediaType.WILDCARD])) - def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [BANNER])) + def secondOrtb2BlockingOverride = new Ortb2BlockingOverride(override: [secondOverrideAttributes], conditions: new Ortb2BlockingConditions(mediaType: [bidRequest.imp[0].mediaTypes[0]])) def ortb2BlockingAttributeConfig = Ortb2BlockingAttributeConfig.getDefaultConfig([ortb2Attributes], attributeName).tap { enforceBlocks = true actionOverrides = Ortb2BlockingActionOverride.getDefaultOverride(attributeName, [firstOrtb2BlockingOverride, secondOrtb2BlockingOverride], null) @@ -1096,12 +1273,14 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BADV PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BAPP PBSUtils.randomString | PBSUtils.randomString | PBSUtils.randomString | BCAT - PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | BANNER_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | VIDEO_BATTR + PBSUtils.randomNumber | PBSUtils.randomNumber | PBSUtils.randomNumber | AUDIO_BATTR } def "PBS should merge allowed bundle for deals overrides together"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultBidRequest + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName) and: "Account in the DB with blocking configuration" def dealId = PBSUtils.randomNumber @@ -1138,11 +1317,16 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { ortb2Attributes | attributeName [PBSUtils.randomString, PBSUtils.randomString] | BADV [PBSUtils.randomString, PBSUtils.randomString] | BCAT - [PBSUtils.randomNumber, PBSUtils.randomNumber] | BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | BANNER_BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | VIDEO_BATTR + [PBSUtils.randomNumber, PBSUtils.randomNumber] | AUDIO_BATTR } def "PBS should not be override from config when ortb2 attribute present in incoming request"() { - given: "Account in the DB with blocking configuration" + given: "Default bid request with proper ortb attribute" + def bidRequest = getBidRequestForOrtbAttribute(attributeName, bidRequestAttribute) + + and: "Account in the DB with blocking configuration" def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attributes], attributeName) accountDao.save(account) @@ -1166,17 +1350,19 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { assert !response?.ext?.prebid?.modules?.warnings where: - bidRequest | ortb2Attributes | attributeName - BidRequest.defaultBidRequest.tap { badv = [PBSUtils.randomString] } | PBSUtils.randomString | BADV - BidRequest.defaultBidRequest.tap { bapp = [PBSUtils.randomString] } | PBSUtils.randomString | BAPP - BidRequest.defaultBidRequest.tap { bcat = [PBSUtils.randomString] } | PBSUtils.randomString | BCAT - BidRequest.defaultBidRequest.tap { imp[0].banner.battr = [PBSUtils.randomNumber] } | PBSUtils.randomNumber | BATTR - BidRequest.defaultBidRequest.tap { imp[0].banner.btype = [PBSUtils.randomNumber] } | PBSUtils.randomNumber | BTYPE + bidRequestAttribute | ortb2Attributes | attributeName + [PBSUtils.randomString] | PBSUtils.randomString | BADV + [PBSUtils.randomString] | PBSUtils.randomString | BAPP + [PBSUtils.randomString] | PBSUtils.randomString | BCAT + [PBSUtils.randomNumber] | PBSUtils.randomNumber | BANNER_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | VIDEO_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | AUDIO_BATTR + [PBSUtils.randomNumber] | PBSUtils.randomNumber | BTYPE } def "PBS should populate seatNonBid when returnAllBidStatus=true and requested bidder responded with rejected advertiser blocked status code"() { given: "Default bidRequest with returnAllBidStatus attribute" - def bidRequest = BidRequest.defaultBidRequest.tap { + def bidRequest = getBidRequestForOrtbAttribute(BADV).tap { it.ext.prebid.returnAllBidStatus = true } @@ -1217,6 +1403,41 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { new Account(uuid: accountId, config: accountConfig) } + private static BidRequest getBidRequestForOrtbAttribute(Ortb2BlockingAttribute attribute, List attributeValue = null) { + switch (attribute) { + case BADV: + return BidRequest.defaultBidRequest.tap { + badv = attributeValue as List + } + case BAPP: + return BidRequest.defaultBidRequest.tap { + bapp = attributeValue as List + } + case BANNER_BATTR: + return BidRequest.defaultBidRequest.tap { + imp[0].banner.battr = attributeValue as List + } + case VIDEO_BATTR: + return BidRequest.defaultVideoRequest.tap { + imp[0].video.battr = attributeValue as List + } + case AUDIO_BATTR: + return BidRequest.defaultAudioRequest.tap { + imp[0].audio.battr = attributeValue as List + } + case BCAT: + return BidRequest.defaultBidRequest.tap { + bcat = attributeValue as List + } + case BTYPE: + return BidRequest.defaultBidRequest.tap { + imp[0].banner.btype = attributeValue as List + } + default: + throw new IllegalArgumentException("Unknown ortb2 attribute: $attribute") + } + } + private static Bid getBidWithOrtb2Attribute(Imp imp, Object ortb2Attributes, Ortb2BlockingAttribute attributeName) { Bid.getDefaultBid(imp).tap { switch (attributeName) { @@ -1226,7 +1447,13 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { case BAPP: bundle = (ortb2Attributes instanceof List) ? ortb2Attributes.first : ortb2Attributes break - case BATTR: + case BANNER_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case VIDEO_BATTR: + attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] + break + case AUDIO_BATTR: attr = (ortb2Attributes instanceof List) ? ortb2Attributes : [ortb2Attributes] break case BCAT: @@ -1246,8 +1473,12 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { return bidRequest.badv case BAPP: return bidRequest.bapp - case BATTR: + case BANNER_BATTR: return bidRequest.imp[0].banner.battr*.toString() + case VIDEO_BATTR: + return bidRequest.imp[0].video.battr*.toString() + case AUDIO_BATTR: + return bidRequest.imp[0].audio.battr*.toString() case BCAT: return bidRequest.bcat case BTYPE: @@ -1263,7 +1494,11 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { return bid.adomain case BAPP: return [bid.bundle] - case BATTR: + case BANNER_BATTR: + return bid.attr*.toString() + case VIDEO_BATTR: + return bid.attr*.toString() + case AUDIO_BATTR: return bid.attr*.toString() case BCAT: return bid.cat diff --git a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java index c6ae182fabc..8437698b9f0 100644 --- a/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java +++ b/src/test/java/org/prebid/server/privacy/gdpr/vendorlist/VersionedVendorListServiceTest.java @@ -47,7 +47,7 @@ public void versionedVendorListServiceShouldTreatTcfPolicyLessThanFourAsVendorLi @Test public void versionedVendorListServiceShouldTreatTcfPolicyGreaterOrEqualFourAsVendorListSpecificationThree() { // given - final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(4, 100); + final int tcfPolicyVersion = ThreadLocalRandom.current().nextInt(4, 64); final TCString consent = TCStringEncoder.newBuilder() .version(2) .tcfPolicyVersion(tcfPolicyVersion) From 6d8cdc96ebe6c881ff775fd154f6eb3501040c4d Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Thu, 17 Oct 2024 15:21:27 +0200 Subject: [PATCH 100/170] Github: Add CodeQl action support (#3512) --- .github/workflows/codeql-analysis.yml | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000000..a86f0b144a5 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,48 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 21 + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Build with Maven + run: mvn -B package --file extra/pom.xml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 + with: + category: "/language:${{ matrix.language }}" From c0adbd722056265b3f722976a540f75c2c7649cb Mon Sep 17 00:00:00 2001 From: serhiinahornyi Date: Fri, 18 Oct 2024 11:22:21 +0200 Subject: [PATCH 101/170] Prebid Server prepare release 3.14.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index eb64da6bd91..d4c7e2d0d1b 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.14.0-SNAPSHOT + 3.14.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 263056107ba..a74b730a946 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0-SNAPSHOT + 3.14.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 3d14e3e6b51..04ee511a0d5 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0-SNAPSHOT + 3.14.0 fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index d997a0dc4bd..5aa165fd33b 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0-SNAPSHOT + 3.14.0 ortb2-blocking diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 81fa2877d71..342b9f5dcc5 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0-SNAPSHOT + 3.14.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index f3d73d09347..91d3accb7ad 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0-SNAPSHOT + 3.14.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 438a800851c..f5b6339d69c 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.14.0-SNAPSHOT + 3.14.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 123c70cbb3c..3e185ea192a 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.14.0-SNAPSHOT + 3.14.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.14.0 diff --git a/pom.xml b/pom.xml index 75a749fab00..3fc11d3a16e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.14.0-SNAPSHOT + 3.14.0 extra/pom.xml From 87e36d939bdbe70ebaaaf3aa4a99bf05448d150b Mon Sep 17 00:00:00 2001 From: serhiinahornyi Date: Fri, 18 Oct 2024 11:22:21 +0200 Subject: [PATCH 102/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index d4c7e2d0d1b..5f17d237be8 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.14.0 + 3.15.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index a74b730a946..a4b77048c76 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0 + 3.15.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 04ee511a0d5..963b239763e 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0 + 3.15.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 5aa165fd33b..90fe75bac96 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0 + 3.15.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 342b9f5dcc5..802bed7fe04 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0 + 3.15.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 91d3accb7ad..fc852b520f2 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.14.0 + 3.15.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index f5b6339d69c..700902c8834 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.14.0 + 3.15.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 3e185ea192a..6fe748f904a 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.14.0 + 3.15.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.14.0 + HEAD diff --git a/pom.xml b/pom.xml index 3fc11d3a16e..8c5d08b1dc6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.14.0 + 3.15.0-SNAPSHOT extra/pom.xml From 1cb10a3ceac7fc5877282804b659e5cfd4e9344c Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:32:52 +0200 Subject: [PATCH 103/170] BlueSea: Site Support (#3515) --- src/main/resources/bidder-config/bluesea.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/bluesea.yaml b/src/main/resources/bidder-config/bluesea.yaml index 91626852f12..23f6a7a702a 100644 --- a/src/main/resources/bidder-config/bluesea.yaml +++ b/src/main/resources/bidder-config/bluesea.yaml @@ -10,5 +10,8 @@ adapters: - video - native site-media-types: + - banner + - video + - native supported-vendors: - vendor-id: 0 + vendor-id: 1294 From c2af8b34b77b86805750074bd78634f6ab0a5dc8 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:40:48 +0300 Subject: [PATCH 104/170] Tests: Fix invalid functional test (#3519) --- .../model/bidder/GeneralBidderAdapter.groovy | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy index dbd47dcda4d..3184fb17fee 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy @@ -1,16 +1,7 @@ package org.prebid.server.functional.model.bidder -import com.fasterxml.jackson.annotation.JsonProperty +class GeneralBidderAdapter extends Generic { -class GeneralBidderAdapter implements BidderAdapter { - - Object exampleProperty - Integer firstParam - Integer secondParam - @JsonProperty("dealsonly") - Boolean dealsOnly - @JsonProperty("pgdealsonly") - Boolean pgDealsOnly String siteId List size String sid From 4f6ee8e0922dfc6cc152a3bbdb7138a745475c5e Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:29:07 +0300 Subject: [PATCH 105/170] Tests: Increase default number for `tests.max-container-count` (#3518) --- docs/developers/functional-tests.md | 2 +- pom.xml | 2 +- .../server/functional/testcontainers/PbsServiceFactory.groovy | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developers/functional-tests.md b/docs/developers/functional-tests.md index c9e827b370a..523466fb0b0 100644 --- a/docs/developers/functional-tests.md +++ b/docs/developers/functional-tests.md @@ -70,7 +70,7 @@ Functional tests need to have name template **.\*Spec.groovy** **Properties:** `launchContainers` - responsible for starting the MockServer and the MySQLContainer container. Default value is false to not launch containers for unit tests. -`tests.max-container-count` - maximum number of simultaneously running PBS containers. Default value is 2. +`tests.max-container-count` - maximum number of simultaneously running PBS containers. Default value is 5. `skipFunctionalTests` - allow to skip funtional tests. Default value is false. `skipUnitTests` - allow to skip unit tests. Default value is false. diff --git a/pom.xml b/pom.xml index 8c5d08b1dc6..e674293243e 100644 --- a/pom.xml +++ b/pom.xml @@ -645,7 +645,7 @@ ${mockserver.version} ${project.version} - 2 + 5 false diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy index a0c5631aac6..e0911a2b1ca 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsServiceFactory.groovy @@ -64,6 +64,6 @@ class PbsServiceFactory { private static int getMaxContainerCount() { USE_FIXED_CONTAINER_PORTS ? 1 - : SystemProperties.getPropertyOrDefault("tests.max-container-count", 2) + : SystemProperties.getPropertyOrDefault("tests.max-container-count", 5) } } From b5815feb5e6f88c320b25efadf57bdbddffab632 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:01:12 +0100 Subject: [PATCH 106/170] Bidders: Triplelift, Unruly, GumGum Bidders Updates (#3513) --- src/main/resources/bidder-config/gumgum.yaml | 1 + src/main/resources/bidder-config/triplelift.yaml | 1 + src/main/resources/bidder-config/tripleliftnative.yaml | 1 + src/main/resources/bidder-config/unruly.yaml | 2 ++ .../it/openrtb2/gumgum/test-auction-gumgum-request.json | 4 +--- .../server/it/openrtb2/gumgum/test-gumgum-bid-request.json | 4 +--- .../openrtb2/triplelift/test-auction-triplelift-request.json | 4 +--- .../it/openrtb2/triplelift/test-triplelift-bid-request.json | 4 +--- .../test-auction-triplelift-native-request.json | 4 +--- .../tripleliftnative/test-triplelift-native-bid-request.json | 4 +--- .../it/openrtb2/unruly/test-auction-unruly-request.json | 4 +--- .../server/it/openrtb2/unruly/test-unruly-bid-request.json | 4 +--- 12 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/main/resources/bidder-config/gumgum.yaml b/src/main/resources/bidder-config/gumgum.yaml index e6bd1d4e98e..b3ee922dce4 100644 --- a/src/main/resources/bidder-config/gumgum.yaml +++ b/src/main/resources/bidder-config/gumgum.yaml @@ -1,6 +1,7 @@ adapters: gumgum: endpoint: https://g2.gumgum.com/providers/prbds2s/bid + ortb-version: "2.6" meta-info: maintainer-email: prebid@gumgum.com app-media-types: diff --git a/src/main/resources/bidder-config/triplelift.yaml b/src/main/resources/bidder-config/triplelift.yaml index e8c35e3eb2c..446825e33dc 100644 --- a/src/main/resources/bidder-config/triplelift.yaml +++ b/src/main/resources/bidder-config/triplelift.yaml @@ -1,6 +1,7 @@ adapters: triplelift: endpoint: https://tlx.3lift.com/s2s/auction?sra=1&supplier_id=20 + ortb-version: "2.6" endpoint-compression: gzip meta-info: maintainer-email: prebid@triplelift.com diff --git a/src/main/resources/bidder-config/tripleliftnative.yaml b/src/main/resources/bidder-config/tripleliftnative.yaml index b090925ff82..e6c1f106f62 100644 --- a/src/main/resources/bidder-config/tripleliftnative.yaml +++ b/src/main/resources/bidder-config/tripleliftnative.yaml @@ -1,6 +1,7 @@ adapters: triplelift_native: endpoint: https://tlx.3lift.com/s2sn/auction?supplier_id=20 + ortb-version: "2.6" meta-info: maintainer-email: prebid@triplelift.com app-media-types: diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml index 050c61c62d9..af4f8690282 100644 --- a/src/main/resources/bidder-config/unruly.yaml +++ b/src/main/resources/bidder-config/unruly.yaml @@ -1,6 +1,8 @@ adapters: unruly: endpoint: https://targeting.unrulymedia.com/unruly_prebid_server + ortb-version: "2.6" + endpoint-compression: gzip meta-info: maintainer-email: prebidsupport@unrulygroup.com app-media-types: diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json index 9c2842a59b6..95c6f56f2cc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json @@ -19,8 +19,6 @@ "buyeruid": "GUM-UID" }, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json index b02e2e91a38..4ac2be5d032 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json @@ -43,9 +43,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json index e6bfa8cab8c..64ac2be961d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json @@ -16,8 +16,6 @@ ], "tmax": 5000, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json index 6a4c678e208..35eb63e37ce 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json @@ -40,9 +40,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json index 64a8c1dc3b5..f49127f75b1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json @@ -24,8 +24,6 @@ }, "tmax": 5000, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json index a738eb40e83..d1199a5a30b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json @@ -40,9 +40,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json index 984635a423f..c11663099d3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json @@ -19,8 +19,6 @@ ], "tmax": 5000, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json index 21763c065aa..a71e9988f62 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json @@ -41,9 +41,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { From 79669e6470715b7ef987ee28ab539c9bd94a6dbc Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:08:25 +0100 Subject: [PATCH 107/170] Github: Add Reviewer Checklist (#3520) --- .github/pull_request_template.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f15985dc6a4..28ce307df13 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,6 +19,15 @@ What's the context for the changes? Are there any Why did you choose to make these changes? Were there any trade-offs you had to consider? +### 🔎 New Bid Adapter Checklist +- [ ] verify email contact works +- [ ] NO fully dynamic hosts +- [ ] geographic host parameters are NOT required +- [ ] NO direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job* +- [ ] if the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter* +- [ ] cover an adapter configuration with an integration test + + ### 🧪 Test plan How do you know the changes are safe to ship to production? From 7269ba834393308423b64760d5d459ca1b3257b4 Mon Sep 17 00:00:00 2001 From: tradplus <58809719+tradplus@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:14:24 +0800 Subject: [PATCH 108/170] Tradplus: New adapter (#3508) --- .../bidder/tradplus/TradPlusBidder.java | 135 +++++++ .../ext/request/tradplus/ExtImpTradPlus.java | 14 + .../bidder/TradPlusBidderConfiguration.java | 41 +++ .../resources/bidder-config/tradplus.yaml | 11 + .../static/bidder-params/tradplus.json | 21 ++ .../bidder/tradplus/TradPlusBidderTest.java | 330 ++++++++++++++++++ .../org/prebid/server/it/TradPlusTest.java | 32 ++ .../test-auction-tradplus-request.json | 24 ++ .../test-auction-tradplus-response.json | 39 +++ .../tradplus/test-tradplus-bid-request.json | 50 +++ .../tradplus/test-tradplus-bid-response.json | 21 ++ .../server/it/test-application.properties | 2 + 12 files changed, 720 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java create mode 100644 src/main/resources/bidder-config/tradplus.yaml create mode 100644 src/main/resources/static/bidder-params/tradplus.json create mode 100644 src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/TradPlusTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java b/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java new file mode 100644 index 00000000000..99d8bc8e74c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/tradplus/TradPlusBidder.java @@ -0,0 +1,135 @@ +package org.prebid.server.bidder.tradplus; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +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 org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.tradplus.ExtImpTradPlus; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class TradPlusBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + public static final String X_OPENRTB_VERSION = "2.5"; + + private static final String ZONE_ID = "{{ZoneID}}"; + private static final String ACCOUNT_ID = "{{AccountID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public TradPlusBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + try { + final ExtImpTradPlus extImpTradPlus = parseImpExt(bidRequest.getImp().getFirst().getExt()); + validateImpExt(extImpTradPlus); + final HttpRequest httpRequest; + httpRequest = makeHttpRequest(extImpTradPlus, bidRequest.getImp(), bidRequest); + return Result.withValue(httpRequest); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpTradPlus parseImpExt(ObjectNode extNode) { + final ExtImpTradPlus extImpTradPlus; + try { + extImpTradPlus = mapper.mapper().convertValue(extNode, EXT_TYPE_REFERENCE).getBidder(); + return extImpTradPlus; + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private void validateImpExt(ExtImpTradPlus extImpTradPlus) { + if (StringUtils.isBlank(extImpTradPlus.getAccountId())) { + throw new PreBidException("Invalid/Missing AccountID"); + } + } + + private HttpRequest makeHttpRequest(ExtImpTradPlus extImpTradPlus, List imps, + BidRequest bidRequest) { + final String uri; + uri = endpointUrl.replace(ZONE_ID, extImpTradPlus.getZoneId()).replace(ACCOUNT_ID, + extImpTradPlus.getAccountId()); + + final BidRequest outgoingRequest = bidRequest.toBuilder().imp(removeImpsExt(imps)).build(); + + return BidderUtil.defaultRequest(outgoingRequest, makeHeaders(), uri, mapper); + } + + private MultiMap makeHeaders() { + return HttpUtil.headers().set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION); + } + + private static List removeImpsExt(List imps) { + return imps.stream().map(imp -> imp.toBuilder().ext(null).build()).toList(); + } + + @Override + public 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())); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, BidRequest bidRequest) { + return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections + .emptyList() : bidsFromResponse(bidResponse, bidRequest.getImp()); + } + + private static List bidsFromResponse(BidResponse bidResponse, List imps) { + return bidResponse.getSeatbid().stream().filter(Objects::nonNull).map(SeatBid::getBid) + .filter(Objects::nonNull).flatMap(Collection::stream).map(bid -> BidderBid + .of(bid, getBidType(bid.getImpid(), imps), bidResponse.getCur())).toList(); + } + + private static BidType getBidType(String impId, List imps) { + for (Imp imp : imps) { + if (imp.getId().equals(impId)) { + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return BidType.banner; + } + } + throw new PreBidException( + "Invalid bid imp ID #%s does not match any imp IDs from the original bid request".formatted(impId)); + } + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java new file mode 100644 index 00000000000..5f20441f9cd --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tradplus/ExtImpTradPlus.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.tradplus; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTradPlus { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java new file mode 100644 index 00000000000..8bd04ffd8f3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/TradPlusBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.tradplus.TradPlusBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import javax.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/tradplus.yaml", factory = YamlPropertySourceFactory.class) +public class TradPlusBidderConfiguration { + + private static final String BIDDER_NAME = "tradplus"; + + @Bean("tradplusConfigurationProperties") + @ConfigurationProperties("adapters.tradplus") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps tradplusBidderDeps(BidderConfigurationProperties tradplusConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(tradplusConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TradPlusBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/tradplus.yaml b/src/main/resources/bidder-config/tradplus.yaml new file mode 100644 index 00000000000..9644f025c45 --- /dev/null +++ b/src/main/resources/bidder-config/tradplus.yaml @@ -0,0 +1,11 @@ +adapters: + tradplus: + endpoint: "https://{{ZoneID}}adx.tradplusad.com/{{AccountID}}/pserver" + meta-info: + maintainer-email: "tpxcontact@tradplus.com" + app-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/tradplus.json b/src/main/resources/static/bidder-params/tradplus.json new file mode 100644 index 00000000000..deae1392d1d --- /dev/null +++ b/src/main/resources/static/bidder-params/tradplus.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "TradPlus Adapter Params", + "description": "A schema which validates params accepted by the TradPlus adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Account ID", + "minLength": 1 + }, + "zoneId": { + "type": "string", + "description": "Zone ID" + } + }, + "required": [ + "accountId", + "zoneId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java b/src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java new file mode 100644 index 00000000000..12384199d76 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/tradplus/TradPlusBidderTest.java @@ -0,0 +1,330 @@ +package org.prebid.server.bidder.tradplus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.tradplus.ExtImpTradPlus; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +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.prebid.server.bidder.tradplus.TradPlusBidder.X_OPENRTB_VERSION; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; +import static org.prebid.server.util.HttpUtil.X_OPENRTB_VERSION_HEADER; + +public class TradPlusBidderTest extends VertxTest { + + private static final String ENDPOINT_TEMPLATE = "http://{{ZoneID}}/openrtb2?sid={{AccountID}}"; + + private final TradPlusBidder target = new TradPlusBidder(ENDPOINT_TEMPLATE, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new TradPlusBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldRemoveAllImpExt() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("impId1"), + imp -> imp.id("impId2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsOnlyNulls(); + } + + @Test + public void makeHttpRequestsShouldMakeSingleRequestForAllImps() { + + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .extracting(List::size) + .containsOnly(2); + + assertThat(result.getValue()).hasSize(1) + .flatExtracting(HttpRequest::getImpIds) + .containsOnly("givenImp1", "givenImp2"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(X_OPENRTB_VERSION_HEADER)) + .isEqualTo(X_OPENRTB_VERSION)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateCorrectURL() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTradPlus.of("testAccountId", "testZoneId")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getUri()).isEqualTo("http://testZoneId/openrtb2?sid=testAccountId"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Cannot deserialize value"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenAccountIdIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTradPlus.of(null, "testZoneId")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Invalid/Missing AccountID")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenAccountIdIsBlank() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder() + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpTradPlus.of(" ", "testZoneId")))) + .build())) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsOnly(BidderError.badInput("Invalid/Missing AccountID")); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Failed to decode: Unrecognized token"); + assertThat(result.getErrors().getFirst().getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidImpIdIsNotPresent() throws JsonProcessingException { + // given + final BidderCall bidderCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").banner(Banner.builder().build()).build())) + .build(), + givenBidResponse(bidBuilder -> bidBuilder.impid("wrongBlock"))); + + // when + final Result> result = target.makeBids(bidderCall, null); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badServerResponse( + "Invalid bid imp ID #wrongBlock does not match any imp IDs from the original bid request")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder().id("123").build())) + .build(), + 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")); + } + + @Test + public void makeBidsShouldReturnVideoBidIfVideoIsPresent() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .video(Video.builder().build()) + .id("123") + .build())) + .build(), + 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")); + } + + @Test + public void makeBidsShouldReturnNativeBidIfNativeIsPresent() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + BidRequest.builder() + .imp(singletonList(Imp.builder() + .xNative(Native.builder().build()) + .id("123") + .build())) + .build(), + 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(), xNative, "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(TradPlusBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, + ExtImpTradPlus.of("accountId", "zoneId"))))) + .build(); + } + + private static String givenBidResponse(Function bidCustomizer) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build()); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/TradPlusTest.java b/src/test/java/org/prebid/server/it/TradPlusTest.java new file mode 100644 index 00000000000..894bc3e4da3 --- /dev/null +++ b/src/test/java/org/prebid/server/it/TradPlusTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +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; +import static java.util.Collections.singletonList; + +public class TradPlusTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTradPlus() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/accountTestID/tradplus-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/tradplus/test-tradplus-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/tradplus/test-tradplus-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/tradplus/test-auction-tradplus-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/tradplus/test-auction-tradplus-response.json", response, singletonList("tradplus")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-request.json new file mode 100644 index 00000000000..bea2848cd57 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tradplus": { + "accountId": "accountTestID", + "zoneId": "" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json new file mode 100644 index 00000000000..9a101b0e81e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "mtype": 1, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "tradplus", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "tradplus": "{{ tradplus.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-request.json new file mode 100644 index 00000000000..669311b0146 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-request.json @@ -0,0 +1,50 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "secure": 1 + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-response.json new file mode 100644 index 00000000000..891438f2401 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-tradplus-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "mtype": 1, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "h": 250, + "w": 300 + } + ] + } + ] +} 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 2ad3dec0705..485e32c5092 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -455,6 +455,8 @@ adapters.telaria.enabled=true adapters.telaria.endpoint=http://localhost:8090/telaria-exchange/ adapters.theadx.enabled=true adapters.theadx.endpoint=http://localhost:8090/theadx-exchange +adapters.tradplus.enabled=true +adapters.tradplus.endpoint=http://{{ZoneID}}localhost:8090/{{AccountID}}/tradplus-exchange adapters.thetradedesk.enabled=true adapters.thetradedesk.endpoint=http://localhost:8090/thetradedesk-exchange/{{SupplyId}} adapters.thetradedesk.extra-info.supply-id=somesupplyid From 508180f45439b3536aec40c5aaadbc98cd54a385 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:28:29 +0100 Subject: [PATCH 109/170] Displayio: Bidfloor Validation Update (#3516) --- .../bidder/displayio/DisplayioBidder.java | 7 ++----- .../bidder/displayio/DisplayioBidderTest.java | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java index b0207293e4d..fe0e8ad6a03 100644 --- a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java +++ b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java @@ -102,11 +102,8 @@ private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { final BigDecimal bidFloor = imp.getBidfloor(); final String bidFloorCurrency = imp.getBidfloorcur(); - if (!BidderUtil.isValidPrice(bidFloor)) { - throw new PreBidException("BidFloor should be defined"); - } - - if (StringUtils.isNotBlank(bidFloorCurrency) + if (BidderUtil.isValidPrice(bidFloor) + && StringUtils.isNotBlank(bidFloorCurrency) && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) { return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); } diff --git a/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java b/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java index a88bf18e406..fe403f14fb9 100644 --- a/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java @@ -88,16 +88,22 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldReturnErrorWhenBidFloorIsMissing() { + public void makeHttpRequestsShouldSetDefaultCurrencyEvenWhenBidfloorIsAbsent() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null)); + final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null).bidfloorcur("EUR")); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).containsOnly(BidderError.badInput("BidFloor should be defined")); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(null, "USD")); + + verifyNoInteractions(currencyConversionService); } @Test @@ -156,8 +162,7 @@ public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { // given final BidRequest bidRequest = givenBidRequest( imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), - imp -> imp.id("impId2").bidfloor(null), - imp -> imp.id("impId3")); + imp -> imp.id("impId2")); //when final Result>> result = target.makeHttpRequests(bidRequest); @@ -167,7 +172,7 @@ public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getId) - .containsExactly("impId3"); + .containsExactly("impId2"); } @Test From ec488dbad52f17b142ef1933c82988edd2808676 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Mon, 28 Oct 2024 13:40:58 +0200 Subject: [PATCH 110/170] Core: Update PBC integration (#3499) --- .../prebid/server/cache/CoreCacheService.java | 24 ++++-- .../spring/config/ServiceConfiguration.java | 4 + .../scaffolding/PrebidCache.groovy | 4 + .../server/functional/tests/CacheSpec.groovy | 47 ++++++++++ .../server/cache/CoreCacheServiceTest.java | 85 ++++++++++++++++++- 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index bcf839cd383..5d5034e23ce 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.response.Bid; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; @@ -61,8 +62,6 @@ public class CoreCacheService { private static final Logger logger = LoggerFactory.getLogger(CoreCacheService.class); - private static final Map> DEBUG_HEADERS = - HttpUtil.toDebugHeaders(CacheServiceUtil.CACHE_HEADERS); private static final String BID_WURL_ATTRIBUTE = "wurl"; private final HttpClient httpClient; @@ -76,11 +75,16 @@ public class CoreCacheService { private final UUIDIdGenerator idGenerator; private final JacksonMapper mapper; + private final MultiMap cacheHeaders; + private final Map> debugHeaders; + public CoreCacheService( HttpClient httpClient, URL endpointUrl, String cachedAssetUrlTemplate, long expectedCacheTimeMs, + String apiKey, + boolean isApiKeySecured, VastModifier vastModifier, EventsService eventsService, Metrics metrics, @@ -98,6 +102,11 @@ public CoreCacheService( this.clock = Objects.requireNonNull(clock); this.idGenerator = Objects.requireNonNull(idGenerator); this.mapper = Objects.requireNonNull(mapper); + + cacheHeaders = isApiKeySecured + ? HttpUtil.headers().add(HttpUtil.X_PBC_API_KEY_HEADER, Objects.requireNonNull(apiKey)) + : HttpUtil.headers(); + debugHeaders = HttpUtil.toDebugHeaders(cacheHeaders); } public String getEndpointHost() { @@ -121,7 +130,10 @@ public String cacheVideoDebugLog(CachedDebugLog cachedDebugLog, Integer videoCac final List cachedCreatives = Collections.singletonList( makeDebugCacheCreative(cachedDebugLog, cacheKey, videoCacheTtl)); final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); - httpClient.post(endpointUrl.toString(), HttpUtil.headers(), mapper.encodeToString(bidCacheRequest), + httpClient.post( + endpointUrl.toString(), + cacheHeaders, + mapper.encodeToString(bidCacheRequest), expectedCacheTimeMs); return cacheKey; } @@ -155,7 +167,7 @@ private Future makeRequest(BidCacheRequest bidCacheRequest, final long startTime = clock.millis(); return httpClient.post( endpointUrl.toString(), - CacheServiceUtil.CACHE_HEADERS, + cacheHeaders, mapper.encodeToString(bidCacheRequest), remainingTimeout) .map(response -> toBidCacheResponse( @@ -286,7 +298,7 @@ private Future doCacheOpenrtb(List bids, final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body); final long startTime = clock.millis(); - return httpClient.post(url, CacheServiceUtil.CACHE_HEADERS, body, remainingTimeout) + return httpClient.post(url, cacheHeaders, body, remainingTimeout) .map(response -> processResponseOpenrtb(response, httpRequest, cachedCreatives.size(), @@ -348,7 +360,7 @@ private DebugHttpCall makeDebugHttpCall(String endpoint, .responseStatus(httpResponse != null ? httpResponse.getStatusCode() : null) .responseBody(httpResponse != null ? httpResponse.getBody() : null) .responseTimeMillis(responseTime(startTime)) - .requestHeaders(DEBUG_HEADERS) + .requestHeaders(debugHeaders) .build(); } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 5676d7fd43e..71e014fcc23 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -160,6 +160,8 @@ CoreCacheService cacheService( @Value("${cache.path}") String path, @Value("${cache.query}") String query, @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, + @Value("${pbc.api.key:#{null}}") String apiKey, + @Value("${cache.api-key-secured:false}") boolean apiKeySecured, VastModifier vastModifier, EventsService eventsService, HttpClient httpClient, @@ -172,6 +174,8 @@ CoreCacheService cacheService( CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), CacheServiceUtil.getCachedAssetUrlTemplate(scheme, host, path, query), expectedCacheTimeMs, + apiKey, + apiKeySecured, vastModifier, eventsService, metrics, diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy index 1c47147f596..224f7c8b228 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy @@ -52,6 +52,10 @@ class PrebidCache extends NetworkScaffolding { .collect { decode(it.body.toString(), BidCacheRequest) } } + Map> getRequestHeaders(String impId) { + getLastRecordedRequestHeaders(getRequest(impId)) + } + @Override HttpRequest getRequest() { request().withMethod("POST") diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy index ccd5f8b9cf0..4f8dcf7675e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -19,6 +19,8 @@ import static org.prebid.server.functional.model.response.auction.MediaType.VIDE class CacheSpec extends BaseSpec { + private final static String PBS_API_HEADER = 'x-pbc-api-key' + def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() { given: "Current value of metric prebid_cache.requests.ok" def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok") @@ -87,6 +89,51 @@ class CacheSpec extends BaseSpec { then: "PBS should call PBC" assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call shouldn't include api-key" + assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + } + + def "PBS should cache bids without api-key header when targeting is specified and api-key-secured disabled"() { + given: "Pbs config with disabled api-key-secured and pbc.api.key" + def apiKey = PBSUtils.randomString + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'false']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.enableCache() + bidRequest.ext.prebid.targeting = new Targeting() + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + prebidCache.getRequest() + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call shouldn't include api-key" + assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + } + + def "PBS should cache bids with api-key header when targeting is specified and api-key-secured enabled"() { + given: "Pbs config with api-key-secured and pbc.api.key" + def apiKey = PBSUtils.randomString + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'true']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.enableCache() + bidRequest.ext.prebid.targeting = new Targeting() + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + prebidCache.getRequest() + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS call should include api-key" + assert prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] == [apiKey] } def "PBS should not cache bids when targeting isn't specified"() { diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index 8a55773a0c0..a8ce602aeb5 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -8,6 +8,7 @@ import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,6 +41,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.settings.model.Account; +import org.prebid.server.util.HttpUtil; import org.prebid.server.vast.VastModifier; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; @@ -107,6 +109,8 @@ public void setUp() throws MalformedURLException, JsonProcessingException { new URL("http://cache-service/cache"), "http://cache-service-host/cache?uuid=", 100L, + null, + false, vastModifier, eventsService, metrics, @@ -371,6 +375,40 @@ public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessin .build()); } + @Test + public void cacheBidsOpenrtbShouldUseApiKeyWhenProvided() throws MalformedURLException { + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + "ApiKey", + true, + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); + + // when + final Future future = target.cacheBidsOpenrtb( + singletonList(bidinfo), + givenAuctionContext(), + CacheContext.builder() + .shouldCacheBids(true) + .build(), + eventsContext); + + // then + assertThat(future.result().getHttpCall().getRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString())) + .containsExactly("ApiKey"); + assertThat(captureBidCacheRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString())) + .isEqualTo("ApiKey"); + } + @Test public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() { // given @@ -694,7 +732,7 @@ public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty( } @Test - public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOException { + public void cachePutObjectsShould() throws IOException { // given final BidPutObject firstBidPutObject = BidPutObject.builder() .type("json") @@ -762,6 +800,45 @@ public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOExcepti .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); } + @Test + public void cachePutObjectsShouldUseApiKeyWhenProvided() throws MalformedURLException { + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + "ApiKey", + true, + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + + final BidPutObject firstBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .build(); + + // when + target.cachePutObjects( + asList(firstBidPutObject), + true, + singleton("bidder1"), + "account", + "pbjs", + timeout); + + // then + assertThat(captureBidCacheRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString())) + .isEqualTo("ApiKey"); + } + private AuctionContext givenAuctionContext(UnaryOperator accountCustomizer, UnaryOperator bidRequestCustomizer) { @@ -850,6 +927,12 @@ private BidCacheRequest captureBidCacheRequest() throws IOException { return mapper.readValue(captor.getValue(), BidCacheRequest.class); } + private MultiMap captureBidCacheRequestHeaders() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient).post(anyString(), captor.capture(), anyString(), anyLong()); + return captor.getValue(); + } + private Map> givenDebugHeaders() { final Map> headers = new HashMap<>(); headers.put("Accept", singletonList("application/json")); From 1d26a183123e17f4b9827b4354c85964f1111d25 Mon Sep 17 00:00:00 2001 From: Oleksandr Balashov Date: Wed, 30 Oct 2024 15:42:49 +0200 Subject: [PATCH 111/170] Loopme: Update bidder params (#3529) --- src/main/resources/static/bidder-params/loopme.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/static/bidder-params/loopme.json b/src/main/resources/static/bidder-params/loopme.json index 89d95d8c011..5ea22ec7ba5 100644 --- a/src/main/resources/static/bidder-params/loopme.json +++ b/src/main/resources/static/bidder-params/loopme.json @@ -20,5 +20,5 @@ "minLength": 1 } }, - "required": ["publisherId", "bundleId", "placementId"] + "required": ["publisherId"] } From 87fbbe992b8006caa9beeb5b6043a10a85622c62 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Thu, 31 Oct 2024 13:32:27 +0200 Subject: [PATCH 112/170] Docs: Added docs for admin endpoints. (#3531) --- docs/admin-endpoints.md | 209 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/admin-endpoints.md diff --git a/docs/admin-endpoints.md b/docs/admin-endpoints.md new file mode 100644 index 00000000000..b3176a4379c --- /dev/null +++ b/docs/admin-endpoints.md @@ -0,0 +1,209 @@ +# Admin enpoints + +Prebid Server Java offers a set of admin endpoints for managing and monitoring the server's health, configurations, and +metrics. Below is a detailed description of each endpoint, including HTTP methods, paths, parameters, and responses. + +## General settings + +Each endpoint can be either enabled or disabled by changing `admin-endpoints..enabled` toggle. Defaults to +`false`. + +Each endpoint can be configured to serve either on application port (configured via `server.http.port` setting) or +admin port (configured via `admin.port` setting) by changing `admin-endpoints..on-application-port` +setting. +By default, all admin endpoints reside on admin port. + +Each endpoint can be configured to serve on a certain path by setting `admin-endpoints..path`. + +Each endpoint can be configured to either require basic authorization or not by changing +`admin-endpoints..protected` setting, +defaults to `true`. Allowed credentials are globally configured for all admin endpoints with +`admin-endpoints.credentials.` +setting. + +## Endpoints + +1. Version info + +- Name: version +- Endpoint: Configured via `admin-endpoints.version.path` setting +- Methods: + - `GET`: + - Description: Returns the version information for the Prebid Server Java instance. + - Parameters: None + - Responses: + - 200 OK: JSON containing version details + ```json + { + "version": "x.x.x", + "revision": "commit-hash" + } + ``` + +2. Currency rates + +- Name: currency-rates +- Methods: + - `GET`: + - Description: Returns the latest information about currency rates used by server instance. + - Parameters: None + - Responses: + - 200 OK: JSON containing version details + ```json + { + "active": "true", + "source": "http://currency-source" + "fetchingIntervalNs": 200, + "lastUpdated": "02/01/2018 - 13:45:30 UTC" + ... Rates ... + } + ``` + +3. Cache notification endpoint + +- Name: storedrequest +- Methods: + - `POST`: + - Description: Updates stored requests/imps data stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": { + "": "", + ... Requests data ... + }, + "imps": { + "": "", + ... Imps data ... + } + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + - `DELETE`: + - Description: Invalidates stored requests/imps data stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": ["", ... Request names ...], + "imps": ["", ... Imp names ...] + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + +4. Amp cache notification endpoint + +- Name: storedrequest-amp +- Methods: + - `POST`: + - Description: Updates stored requests/imps data for amp, stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": { + "": "", + ... Requests data ... + }, + "imps": { + "": "", + ... Imps data ... + } + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + - `DELETE`: + - Description: Invalidates stored requests/imps data for amp, stored in server instance cache. + - Parameters: + - body: + ```json + { + "requests": ["", ... Request names ...], + "imps": ["", ... Imp names ...] + } + ``` + - Responses: + - 200 OK + - 400 BAD REQUEST + - 405 METHOD NOT ALLOWED + +5. Account cache notification endpoint + +- Name: cache-invalidation +- Methods: + - any: + - Description: Invalidates cached data for a provided account in server instance cache. + - Parameters: + - `account`: Account id. + - Responses: + - 200 OK + - 400 BAD REQUEST + + +6. Http interaction logging endpoint + +- Name: logging-httpinteraction +- Methods: + - any: + - Description: Changes request logging specification in server instance. + - Parameters: + - `endpoint`: Endpoint. Should be either: `auction` or `amp`. + - `statusCode`: Status code for logging spec. + - `account`: Account id. + - `bidder`: Bidder code. + - `limit`: Limit of requests for specification to be valid. + - Responses: + - 200 OK + - 400 BAD REQUEST +- Additional settings: + - `logging.http-interaction.max-limit` - max limit for logging specification limit. + +7. Logging level control endpoint + +- Name: logging-changelevel +- Methods: + - any: + - Description: Changes request logging level for specified amount of time in server instance. + - Parameters: + - `level`: Logging level. Should be one of: `all`, `trace`, `debug`, `info`, `warn`, `error`, `off`. + - `duration`: Duration of logging level (in millis) before reset to original one. + - Responses: + - 200 OK + - 400 BAD REQUEST +- Additional settings: + - `logging.change-level.max-duration-ms` - max duration of changed logger level. + +8. Tracer log endpoint + +- Name: tracelog +- Methods: + - any: + - Description: Adds trace logging specification for specified amount of time in server instance. + - Parameters: + - `account`: Account id. + - `bidderCode`: Bidder code. + - `level`: Log level. Should be one of: `info`, `warn`, `trace`, `error`, `fatal`, `debug`. + - `duration`: Duration of logging specification (in seconds). + - Responses: + - 200 OK + - 400 BAD REQUEST + +9. Collected metrics endpoint + +- Name: collected-metrics +- Methods: + - any: + - Description: Adds trace logging specification for specified amount of time in server instance. + - Parameters: None + - Responses: + - 200 OK: JSON containing metrics data. From 4ca292e5216519287f414cafaa4cce530926ab32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20M=C3=BCller?= <449563+steffenmllr@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:47:04 +0100 Subject: [PATCH 113/170] Agma: Bugfixes (#3495) --- .../reporter/agma/AgmaAnalyticsReporter.java | 14 ++- .../spring/config/AnalyticsConfiguration.java | 14 ++- .../agma/AgmaAnalyticsReporterTest.java | 118 ++++++++++++++++++ 3 files changed, 138 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java index 93665840a21..9c3252d4116 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -16,6 +16,7 @@ import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.model.AmpEvent; @@ -146,11 +147,7 @@ public Future processEvent(T event) { final String eventString = jacksonMapper.encodeToString(agmaEvent); buffer.put(eventString, eventString.length()); - final List toFlush = buffer.pollToFlush(); - if (!toFlush.isEmpty()) { - sendEvents(toFlush); - } - + sendEvents(buffer.pollToFlush()); return Future.succeededFuture(); } @@ -200,10 +197,15 @@ private static String getPublisherId(BidRequest bidRequest) { return null; } - return publisherId; + return StringUtils.isNotBlank(appSiteId) + ? String.format("%s_%s", StringUtils.defaultString(publisherId), appSiteId) + : publisherId; } private void sendEvents(List events) { + if (events.isEmpty()) { + return; + } final String payload = preparePayload(events); final Future responseFuture = compressToGzip ? httpClient.request(HttpMethod.POST, url, headers, gzip(payload), httpTimeoutMs) diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java index 01153008824..d618c36fa36 100644 --- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java @@ -5,6 +5,7 @@ import lombok.NoArgsConstructor; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.analytics.AnalyticsReporter; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.analytics.reporter.agma.AgmaAnalyticsReporter; @@ -111,8 +112,9 @@ private static class AgmaAnalyticsConfigurationProperties { public AgmaAnalyticsProperties toComponentProperties() { final Map accountsByPublisherId = accounts.stream() .collect(Collectors.toMap( - AgmaAnalyticsAccountProperties::getPublisherId, - AgmaAnalyticsAccountProperties::getCode)); + this::buildPublisherSiteAppIdKey, + AgmaAnalyticsAccountProperties::getCode + )); return AgmaAnalyticsProperties.builder() .url(endpoint.getUrl()) @@ -125,6 +127,14 @@ public AgmaAnalyticsProperties toComponentProperties() { .build(); } + private String buildPublisherSiteAppIdKey(AgmaAnalyticsAccountProperties account) { + final String publisherId = account.getPublisherId(); + final String siteAppId = account.getSiteAppId(); + return StringUtils.isNotBlank(siteAppId) + ? String.format("%s_%s", publisherId, siteAppId) + : publisherId; + } + @Validated @NoArgsConstructor @Data diff --git a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java index 2427942605e..3de67e31b93 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java @@ -408,6 +408,124 @@ public void processEventShouldNotSendAnythingWhenAccountsDoesNotHaveConfiguredPu assertThat(result.succeeded()).isTrue(); } + @Test + public void processEventShouldSendWhenAccountsHasConfiguredAppsOrSites() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("publisherId_bundleId", "accountCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + // given + final App givenApp = App.builder().bundle("bundleId") + .publisher(Publisher.builder().id("publisherId").build()).build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .app(givenApp) + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .app(givenApp) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + any(), + eq(expectedEventPayload), + eq(1000L)); + } + + @Test + public void processEventShouldSendWhenAccountsHasConfiguredAppsOrSitesOnly() { + // given + final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder() + .url("http://endpoint.com") + .gzip(false) + .bufferSize(100000) + .bufferTimeoutMs(10000L) + .maxEventsCount(0) + .httpTimeoutMs(1000L) + .accounts(Map.of("_mySite", "accountCode")) + .build(); + + target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx); + + // given + final Site givenSite = Site.builder().id("mySite").build(); + final Device givenDevice = Device.builder().build(); + final User givenUser = User.builder().build(); + + final AuctionEvent auctionEvent = AuctionEvent.builder() + .auctionContext(AuctionContext.builder() + .privacyContext(PrivacyContext.of( + null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build())) + .timeoutContext(TimeoutContext.of(clock.millis(), null, 1)) + .bidRequest(BidRequest.builder() + .id("requestId") + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .build()) + .build()) + .build(); + + // when + final Future result = target.processEvent(auctionEvent); + + // then + final AgmaEvent expectedEvent = AgmaEvent.builder() + .eventType("auction") + .accountCode("accountCode") + .requestId("requestId") + .site(givenSite) + .device(givenDevice) + .user(givenUser) + .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00")) + .build(); + + final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]"; + + verify(httpClient).request( + eq(POST), + eq("http://endpoint.com"), + any(), + eq(expectedEventPayload), + eq(1000L)); + } + @Test public void processEventShouldSendEncodingGzipHeaderAndCompressedPayload() { // given From af321ad62f1db70b69ce991a2ce6894a058056c6 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Mon, 4 Nov 2024 13:54:17 +0100 Subject: [PATCH 114/170] Improvedigital: Remove consented_providers logic (#3534) --- .../improvedigital/ImprovedigitalBidder.java | 52 ------------------- .../static/bidder-params/improvedigital.json | 4 +- .../ImprovedigitalBidderTest.java | 22 -------- .../test-improvedigital-bid-request.json | 7 --- 4 files changed, 3 insertions(+), 82 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java index a2cba1d3c36..e86a6182eb9 100644 --- a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java +++ b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java @@ -4,11 +4,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -26,13 +24,10 @@ 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.ConsentedProvidersSettings; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.improvedigital.ExtImpImprovedigital; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; import java.util.ArrayList; import java.util.Collection; @@ -50,9 +45,6 @@ public class ImprovedigitalBidder implements Bidder { private static final TypeReference> IMPROVEDIGITAL_EXT_TYPE_REFERENCE = new TypeReference<>() { }; - private static final String CONSENT_PROVIDERS_SETTINGS_OUT_KEY = "consented_providers_settings"; - private static final String CONSENTED_PROVIDERS_KEY = "consented_providers"; - private static final String REGEX_SPLIT_STRING_BY_DOT = "\\."; private static final String IS_REWARDED_INVENTORY_FIELD = "is_rewarded_inventory"; private static final JsonPointer IS_REWARDED_INVENTORY_POINTER @@ -89,46 +81,6 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValues(httpRequests); } - private ExtUser getAdditionalConsentProvidersUserExt(ExtUser extUser) { - final String consentedProviders = ObjectUtil.getIfNotNull( - ObjectUtil.getIfNotNull(extUser, ExtUser::getConsentedProvidersSettings), - ConsentedProvidersSettings::getConsentedProviders); - - if (StringUtils.isBlank(consentedProviders)) { - return extUser; - } - - final String[] consentedProvidersParts = StringUtils.split(consentedProviders, "~"); - final String consentedProvidersPart = consentedProvidersParts.length > 1 ? consentedProvidersParts[1] : null; - if (StringUtils.isBlank(consentedProvidersPart)) { - return extUser; - } - - return fillExtUser(extUser, consentedProvidersPart.split(REGEX_SPLIT_STRING_BY_DOT)); - } - - private ExtUser fillExtUser(ExtUser extUser, String[] arrayOfSplitString) { - final JsonNode consentProviderSettingJsonNode; - try { - consentProviderSettingJsonNode = customJsonNode(arrayOfSplitString); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - - return mapper.fillExtension(extUser, consentProviderSettingJsonNode); - } - - private JsonNode customJsonNode(String[] arrayOfSplitString) { - final Integer[] integers = mapper.mapper().convertValue(arrayOfSplitString, Integer[].class); - final ArrayNode arrayNode = mapper.mapper().createArrayNode(); - for (Integer integer : integers) { - arrayNode.add(integer); - } - - return mapper.mapper().createObjectNode().set(CONSENT_PROVIDERS_SETTINGS_OUT_KEY, - mapper.mapper().createObjectNode().set(CONSENTED_PROVIDERS_KEY, arrayNode)); - } - private ExtImpImprovedigital parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), IMPROVEDIGITAL_EXT_TYPE_REFERENCE).getBidder(); @@ -149,12 +101,8 @@ private static Imp updateImp(Imp imp) { } private HttpRequest resolveRequest(BidRequest bidRequest, Imp imp, Integer publisherId) { - final User user = bidRequest.getUser(); final BidRequest modifiedRequest = bidRequest.toBuilder() .imp(Collections.singletonList(updateImp(imp))) - .user(user != null - ? user.toBuilder().ext(getAdditionalConsentProvidersUserExt(user.getExt())).build() - : null) .build(); final String pathPrefix = publisherId != null && publisherId > 0 diff --git a/src/main/resources/static/bidder-params/improvedigital.json b/src/main/resources/static/bidder-params/improvedigital.json index 5681d896e92..ecd60a98b1d 100644 --- a/src/main/resources/static/bidder-params/improvedigital.json +++ b/src/main/resources/static/bidder-params/improvedigital.json @@ -35,5 +35,7 @@ "description": "Placement size" } }, - "required": ["placementId"] + "required": [ + "placementId" + ] } diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java index b75ea763ec6..37ab6635458 100644 --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java @@ -192,28 +192,6 @@ public void makeHttpRequestsShouldReturnUserExtIfConsentedProvidersIsNotProvided .containsExactly(extUser); } - @Test - public void makeHttpRequestsShouldReturnErrorIfCannotParseConsentedProviders() { - // given - final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("1~a.fv.90")) - .build(); - - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder - .user(User.builder().ext(extUser).build()).id("request_id"), - identity()); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).allSatisfy(error -> { - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); - assertThat(error.getMessage()).startsWith("Cannot deserialize value of type"); - }); - } - @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json index b79274a221e..05c99b710fc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json @@ -41,13 +41,6 @@ "ext": { "ConsentedProvidersSettings": { "consented_providers": "1~10.20.90" - }, - "consented_providers_settings": { - "consented_providers": [ - 10, - 20, - 90 - ] } } }, From 98e8065dfa4f8ec6fdb34225a7e1c1522098d57b Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:57:23 +0100 Subject: [PATCH 115/170] Price Granularity: Defaults Fix (#3511) --- .../server/auction/PriceGranularity.java | 6 + .../requestfactory/AmpRequestFactory.java | 25 +- .../Ortb2ImplicitParametersResolver.java | 41 +++- .../model/config/AccountAuctionConfig.groovy | 4 +- .../model/config/PriceGranularityType.groovy | 28 +++ .../request/auction/PriceGranularity.groovy | 11 + .../model/request/auction/Range.groovy | 6 + .../functional/tests/TargetingSpec.groovy | 229 ++++++++++++++++++ .../PriceFloorsFetchingSpec.groovy | 1 - .../server/functional/util/PBSUtils.groovy | 6 +- .../server/auction/PriceGranularityTest.java | 13 + .../requestfactory/AmpRequestFactoryTest.java | 29 +++ .../Ortb2ImplicitParametersResolverTest.java | 29 +++ 13 files changed, 406 insertions(+), 22 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy diff --git a/src/main/java/org/prebid/server/auction/PriceGranularity.java b/src/main/java/org/prebid/server/auction/PriceGranularity.java index 81cec03fd62..75620e4d953 100644 --- a/src/main/java/org/prebid/server/auction/PriceGranularity.java +++ b/src/main/java/org/prebid/server/auction/PriceGranularity.java @@ -72,6 +72,12 @@ public static PriceGranularity createFromString(String stringPriceGranularity) { } } + public static PriceGranularity createFromStringOrDefault(String stringPriceGranularity) { + return isValidStringPriceGranularityType(stringPriceGranularity) + ? STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.valueOf(stringPriceGranularity)) + : PriceGranularity.DEFAULT; + } + /** * Returns list of {@link ExtGranularityRange}s. */ diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 0084afc7aca..b014c508678 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -52,6 +52,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -60,6 +61,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; public class AmpRequestFactory { @@ -407,7 +409,7 @@ private Future updateBidRequest(AuctionContext auctionContext) { .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) - .map(this::fillExplicitParameters) + .map(bidRequest -> fillExplicitParameters(bidRequest, account)) .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())) @@ -459,7 +461,7 @@ private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidR * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext} * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters */ - private BidRequest fillExplicitParameters(BidRequest bidRequest) { + private BidRequest fillExplicitParameters(BidRequest bidRequest, Account account) { final List imps = bidRequest.getImp(); // Force HTTPS as AMP requires it, but pubs can forget to set it. final Imp imp = imps.getFirst(); @@ -496,6 +498,7 @@ private BidRequest fillExplicitParameters(BidRequest bidRequest) { .imp(setSecure ? Collections.singletonList(imps.getFirst().toBuilder().secure(1).build()) : imps) .ext(extRequest( bidRequest, + account, setDefaultTargeting, setDefaultCache)) .build(); @@ -692,6 +695,7 @@ private static List parseMultiSizeParam(String ms) { * Creates updated bidrequest.ext {@link ObjectNode}. */ private ExtRequest extRequest(BidRequest bidRequest, + Account account, boolean setDefaultTargeting, boolean setDefaultCache) { @@ -704,7 +708,7 @@ private ExtRequest extRequest(BidRequest bidRequest, : ExtRequestPrebid.builder(); if (setDefaultTargeting) { - prebidBuilder.targeting(createTargetingWithDefaults(prebid)); + prebidBuilder.targeting(createTargetingWithDefaults(prebid, account)); } if (setDefaultCache) { prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null), @@ -727,15 +731,14 @@ private ExtRequest extRequest(BidRequest bidRequest, * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's * child properties is missed or entire targeting does not exist. */ - private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) { + private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNull = targeting == null; final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity(); final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull(); - final JsonNode outgoingPriceGranularityNode - = isPriceGranularityNull - ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)) + final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull + ? mapper.mapper().valueToTree(ExtPriceGranularity.from(getDefaultPriceGranularity(account))) : priceGranularityNode; final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull @@ -759,6 +762,14 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) .build(); } + private static PriceGranularity getDefaultPriceGranularity(Account account) { + return Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + } + @Value(staticConstructor = "of") private static class GppSidExtraction { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java index 8b377c08bf9..5bcabe413db 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -56,6 +56,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -187,7 +189,11 @@ public BidRequest resolve(BidRequest bidRequest, final ExtRequest ext = bidRequest.getExt(); final List imps = bidRequest.getImp(); final ExtRequest populatedExt = populateRequestExt( - ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps), endpoint); + ext, + bidRequest, + ObjectUtils.defaultIfNull(populatedImps, imps), + endpoint, + auctionContext.getAccount()); final Source source = bidRequest.getSource(); final Source populatedSource = populateSource(source, populatedExt, hasStoredBidRequest); @@ -713,10 +719,15 @@ private static boolean isUniqueIds(List imps) { return impIdsSet.size() == impIdsList.size(); } - private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List imps, String endpoint) { + private ExtRequest populateRequestExt(ExtRequest ext, + BidRequest bidRequest, + List imps, + String endpoint, + Account account) { + final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(ext, ExtRequest::getPrebid); - final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps); + final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps, account); final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest, endpoint); @@ -783,7 +794,7 @@ private static void resolveImpMediaTypes(Imp imp, Set impsMediaTypes) { /** * Returns populated {@link ExtRequestTargeting} or null if no changes were applied. */ - private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps) { + private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNotNull = targeting != null; @@ -796,8 +807,12 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List i if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) { return targeting.toBuilder() - .pricegranularity(resolvePriceGranularity(targeting, isPriceGranularityNull, - isPriceGranularityTextual, imps)) + .pricegranularity(resolvePriceGranularity( + targeting, + isPriceGranularityNull, + isPriceGranularityTextual, + imps, + account)) .includewinners(isIncludeWinnersNull || targeting.getIncludewinners()) .includebidderkeys(isIncludeBidderKeysNull ? !isWinningOnly(prebid.getCache()) @@ -822,14 +837,22 @@ private boolean isWinningOnly(ExtRequestPrebidCache cache) { * In case of valid string price granularity replaced it with appropriate custom view. * In case of invalid string value throws {@link InvalidRequestException}. */ - private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, - boolean isPriceGranularityTextual, List imps) { + private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, + boolean isPriceGranularityNull, + boolean isPriceGranularityTextual, + List imps, + Account account) { final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) .containsAll(getImpMediaTypes(imps)); if (isPriceGranularityNull && !hasAllMediaTypes) { - return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); + final PriceGranularity defaultPriceGranularity = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + return mapper.mapper().valueToTree(ExtPriceGranularity.from(defaultPriceGranularity)); } final JsonNode priceGranularityNode = targeting.getPricegranularity(); diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 63d27073805..2983ac3f731 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -12,7 +12,7 @@ import org.prebid.server.functional.model.response.auction.MediaType @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountAuctionConfig { - String priceGranularity + PriceGranularityType priceGranularity Integer bannerCacheTtl Integer videoCacheTtl Integer truncateTargetAttr @@ -28,7 +28,7 @@ class AccountAuctionConfig { PrivacySandbox privacySandbox @JsonProperty("price_granularity") - String priceGranularitySnakeCase + PriceGranularityType priceGranularitySnakeCase @JsonProperty("banner_cache_ttl") Integer bannerCacheTtlSnakeCase @JsonProperty("video_cache_ttl") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy new file mode 100644 index 00000000000..957a2d880bf --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.Range + +enum PriceGranularityType { + + LOW(2, [Range.getDefault(5, 0.5)]), + MEDIUM(2, [Range.getDefault(20, 0.1)]), + MED(2, [Range.getDefault(20, 0.1)]), + HIGH(2, [Range.getDefault(20, 0.01)]), + AUTO(2, [Range.getDefault(5, 0.05), Range.getDefault(10, 0.1), Range.getDefault(20, 0.5)]), + DENSE(2, [Range.getDefault(3, 0.01), Range.getDefault(8, 0.05), Range.getDefault(20, 0.5)]), + UNKNOWN(null, []) + + final Integer precision + final List ranges + + PriceGranularityType(Integer precision, List ranges) { + this.precision = precision + this.ranges = ranges + } + + @JsonValue + String toLowerCase() { + return name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy index 29f4472cab2..873c686a578 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy @@ -1,10 +1,21 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.config.PriceGranularityType @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class PriceGranularity { Integer precision List ranges + + static PriceGranularity getDefault(PriceGranularityType granularity) { + new PriceGranularity(precision: granularity.precision, ranges: granularity.ranges) + } + + static PriceGranularity getDefault() { + getDefault(PriceGranularityType.MED) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy index c5fa8cb2220..1b106b67faa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy @@ -1,10 +1,16 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Range { BigDecimal max BigDecimal increment + + static Range getDefault(Integer max, BigDecimal increment) { + new Range(max: max, increment: increment) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy index 67ff7907901..e24d22b4b8f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.PriceGranularityType import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse @@ -17,13 +18,17 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode import java.nio.charset.StandardCharsets +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.PriceGranularityType.UNKNOWN import static org.prebid.server.functional.model.response.auction.ErrorType.TARGETING import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer @@ -1121,6 +1126,230 @@ class TargetingSpec extends BaseSpec { assert targeting["hb_env"] == HB_ENV_AMP } + def "PBS auction should throw error when price granularity from original request is empty"() { + given: "Default bidRequest with empty price granularity" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS auction should prioritize price granularity from original request over account config"() { + given: "Default bidRequest with price granularity" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity as PriceGranularityType) + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + } + + and: "Account in the DB" + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: PBSUtils.getRandomEnum(PriceGranularityType)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS amp should prioritize price granularity from original request over account config"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity) + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config with different name case when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from default account config when original request doesn't contain price granularity"() { + given: "Pbs with default account that include privacySandbox configuration" + def priceGranularity = PBSUtils.getRandomEnum(PriceGranularityType, [UNKNOWN]) + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def pbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(accountConfig)]) + + and: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + } + + def "PBS auction should include include default price granularity when original request and account config doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include default price granularity" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.default + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(priceGranularity: UNKNOWN)] + } + + def "PBS amp should throw error when price granularity from original request is empty"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest with empty price granularity" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS amp should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + setAccountId(ampRequest.account) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, priceGranularity) + accountDao.save(account) + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + return new Account(uuid: accountId, config: accountConfig) + } + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 8316ee54233..22e5b27a04f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -17,7 +17,6 @@ import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index 0d72dcfe4c6..2db485448b4 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -107,9 +107,9 @@ class PBSUtils implements ObjectMapperWrapper { getRandomDecimal(min, max).setScale(scale, HALF_UP) } - static > T getRandomEnum(Class anEnum) { - def values = anEnum.enumConstants - values[getRandomNumber(0, values.length - 1)] + static > T getRandomEnum(Class anEnum, List exclude = []) { + def values = anEnum.enumConstants.findAll { !exclude.contains(it) } as T[] + values[getRandomNumber(0, values.size() - 1)] } static String convertCase(String input, Case caseType) { diff --git a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java index 8c3fd9ee24b..338af30f830 100644 --- a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java +++ b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java @@ -26,6 +26,19 @@ public void createFromStringShouldThrowPrebidExceptionIfInvalidStringType() { assertThatExceptionOfType(PreBidException.class).isThrownBy(() -> PriceGranularity.createFromString("invalid")); } + @Test + public void createFromStringOrDefaultShouldCreateMedPriceGranularityWhenInvalidStringType() { + // given and when + final PriceGranularity defaultPriceGranularity = PriceGranularity.createFromStringOrDefault( + "invalid"); + + // then + assertThat(defaultPriceGranularity.getRangesMax()).isEqualByComparingTo(BigDecimal.valueOf(20)); + assertThat(defaultPriceGranularity.getPrecision()).isEqualTo(2); + assertThat(defaultPriceGranularity.getRanges()).containsOnly( + ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))); + } + @Test public void createCustomPriceGranularityByStringLow() { // given and when diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index 7318d7906c2..9b716645aa7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -62,6 +62,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.Targeting; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import java.util.ArrayList; @@ -540,6 +542,33 @@ public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestE ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))))); } + @Test + public void shouldReturnBidRequestWithAccountPriceGranularityIfStoredBidRequestExtTargetingHasNoPriceGranularity() { + // given + givenBidRequest( + builder -> builder + .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())), + Imp.builder().build()); + + given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder().priceGranularity("low").build()) + .build())); + + // when + final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(singletonList(request)) + .extracting(BidRequest::getExt).isNotNull() + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity) + // assert that priceGranularity was set with default value and includeWinners remained unchanged + .containsExactly( + tuple(false, mapper.valueToTree(ExtPriceGranularity.of(2, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))))); + } + @Test public void shouldReturnBidRequestWithNotChangedExtRequestPrebidTargetingFields() { // given diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java index 81680a5ac7e..41fa98842e7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java @@ -58,6 +58,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import java.util.ArrayList; @@ -1836,6 +1838,33 @@ public void shouldSetDefaultPriceGranularityIfPriceGranularityAndMediaTypePriceG BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)))))); } + @Test + public void shouldSetAccountPriceGranularityIfPriceGranularityAndMediaTypePriceGranularityIsMissing() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).ext(mapper.createObjectNode()).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder().build()) + .build())) + .build(); + + final AuctionContext givenAuctionContext = auctionContext.with(Account.builder() + .auction(AccountAuctionConfig.builder().priceGranularity("low").build()) + .build()); + + // when + final BidRequest result = target.resolve(bidRequest, givenAuctionContext, ENDPOINT, false); + + // then + assertThat(singletonList(result)) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getPricegranularity) + .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of( + BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))))); + } + @Test public void shouldNotSetDefaultPriceGranularityIfThereIsAMediaTypePriceGranularityForImpType() { // given From 658790cd167d2cfad7a082c9c1d75d0e00f18f3b Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:30:33 +0100 Subject: [PATCH 116/170] Core: Add Zero Non Deal Bids Warning Only in Debug (#3522) --- .../server/auction/ExchangeService.java | 19 ++- .../model/request/auction/BidRequest.groovy | 5 +- .../request/auction/DebugCondition.groovy | 15 ++ .../model/request/auction/Prebid.groovy | 2 +- .../functional/tests/BidValidationSpec.groovy | 137 +++++++++++++++--- .../server/functional/tests/DebugSpec.groovy | 54 +++---- .../functional/tests/SeatNonBidSpec.groovy | 6 +- .../pricefloors/PriceFloorsBaseSpec.groovy | 3 +- .../server/auction/ExchangeServiceTest.java | 32 +++- 9 files changed, 214 insertions(+), 59 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 66b34a8df46..438a0f71acd 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -273,7 +273,8 @@ private Future runAuction(AuctionContext receivedContext) { storedAuctionResponses, bidRequest.getImp(), context.getBidRejectionTrackers())) - .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings)) + .map(auctionParticipations -> dropZeroNonDealBids( + auctionParticipations, debugWarnings, context.getDebugContext().isDebugEnabled())) .map(auctionParticipations -> bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases)) .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases)) @@ -1269,15 +1270,18 @@ private BidderResponse rejectBidderResponseOrProceed(HookStageExecutionResult dropZeroNonDealBids(List auctionParticipations, - List debugWarnings) { + List debugWarnings, + boolean isDebugEnabled) { return auctionParticipations.stream() - .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings)) + .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings, isDebugEnabled)) .toList(); } private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionParticipation, - List debugWarnings) { + List debugWarnings, + boolean isDebugEnabled) { + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); final List bidderBids = seatBid.getBids(); @@ -1287,8 +1291,11 @@ private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionPar final Bid bid = bidderBid.getBid(); if (isZeroNonDealBids(bid.getPrice(), bid.getDealid())) { metrics.updateAdapterRequestErrorMetric(bidderResponse.getBidder(), MetricName.unknown_error); - debugWarnings.add("Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'" - .formatted(bid.getId())); + if (isDebugEnabled) { + debugWarnings.add( + "Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'" + .formatted(bid.getId())); + } } else { validBids.add(bidderBid); } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index 1157580209a..f60cabcc606 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -5,6 +5,7 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE @@ -22,7 +23,7 @@ class BidRequest { Dooh dooh Device device User user - Integer test + DebugCondition test Integer at Long tmax List wseat @@ -63,7 +64,7 @@ class BidRequest { regs = Regs.defaultRegs id = UUID.randomUUID() tmax = 2500 - ext = new BidRequestExt(prebid: new Prebid(debug: 1)) + ext = new BidRequestExt(prebid: new Prebid(debug: ENABLED)) if (channel == SITE) { site = Site.defaultSite } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy new file mode 100644 index 00000000000..066080c56da --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum DebugCondition { + + DISABLED(0), ENABLED(1) + + @JsonValue + final int value + + private DebugCondition(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index 7240fd91719..d99122f180d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -10,7 +10,7 @@ import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) class Prebid { - Integer debug + DebugCondition debug Boolean returnAllBidStatus Map aliases Map aliasgvlids diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy index b2cda35dde1..579a8e16597 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy @@ -18,6 +18,8 @@ import spock.lang.PendingFeature import java.time.Instant import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.util.HttpUtil.REFERER_HEADER @@ -133,7 +135,7 @@ class BidValidationSpec extends BaseSpec { dooh.id = null dooh.venueType = null } - bidDoohRequest.ext.prebid.debug = 1 + bidDoohRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidDoohRequest) @@ -148,7 +150,7 @@ class BidValidationSpec extends BaseSpec { given: "Default basic BidRequest" def bidRequest = BidRequest.defaultBidRequest bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null) - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -159,9 +161,9 @@ class BidValidationSpec extends BaseSpec { } def "PBS should treat bids with 0 price as valid when deal id is present"() { - given: "Default basic BidRequest with generic bidder" + given: "Default basic BidRequest with generic bidder and enabled debug" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Bid response with 0 price bid" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) @@ -183,16 +185,21 @@ class BidValidationSpec extends BaseSpec { } def "PBS should drop invalid bid and emit debug error when bid price is #bidPrice and deal id is #dealId"() { - given: "Default basic BidRequest with generic bidder" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + given: "Default basic BidRequest with generic bidder and enabled debug" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.debug = debug + it.test = test + } and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - def bid = bidResponse.seatbid.first().bid.first() - bid.dealid = dealId - bid.price = bidPrice - def bidId = bid.id + def bidId = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.tap { + id = bidId + dealid = dealId + price = bidPrice + } + } and: "Set bidder response" bidder.setResponse(bidRequest.id, bidResponse) @@ -201,13 +208,61 @@ class BidValidationSpec extends BaseSpec { def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Invalid bid should be deleted" - assert response.seatbid.size() == 0 + assert !response.seatbid + assert !response.ext.seatnonbid and: "PBS should emit an error" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Dropped bid '$bidId'. Does not contain a positive (or zero if there is a deal) 'price'" as String] + where: + debug | test | bidPrice | dealId + DISABLED | ENABLED | PBSUtils.randomNegativeNumber | null + DISABLED | ENABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + DISABLED | ENABLED | 0 | null + DISABLED | ENABLED | null | PBSUtils.randomNumber + DISABLED | ENABLED | null | null + ENABLED | DISABLED | PBSUtils.randomNegativeNumber | null + ENABLED | DISABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + ENABLED | DISABLED | 0 | null + ENABLED | DISABLED | null | PBSUtils.randomNumber + ENABLED | DISABLED | null | null + } + + def "PBS should drop invalid bid without debug error when request debug disabled and bid price is #bidPrice and deal id is #dealId"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + test = DISABLED + ext.prebid.debug = DISABLED + } + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.tap { + dealid = dealId + price = bidPrice + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Invalid bid should be deleted" + assert !response.seatbid + assert !response.ext.seatnonbid + + and: "PBS shouldn't emit an error" + assert !response.ext?.warnings + assert !response.ext?.warnings + + and: "PBS should call bidder" + def bidderRequests = bidder.getBidderRequests(bidResponse.id) + assert bidderRequests.size() == 1 + where: bidPrice | dealId PBSUtils.randomNegativeNumber | null @@ -220,7 +275,7 @@ class BidValidationSpec extends BaseSpec { def "PBS should only drop invalid bid without discarding whole seat"() { given: "Default basic BidRequest with generic bidder" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED bidRequest.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] and: "Bid response with 2 bids" @@ -239,7 +294,7 @@ class BidValidationSpec extends BaseSpec { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) - then: "Invalid bids should be deleted" + then: "Bid response contains only valid bid" assert response.seatbid?.first()?.bid*.id == [validBidId] and: "PBS should emit an error" @@ -247,6 +302,53 @@ class BidValidationSpec extends BaseSpec { assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Dropped bid '$invalidBid.id'. Does not contain a positive (or zero if there is a deal) 'price'" as String] + where: + debug | test | bidPrice | dealId + 0 | 1 | PBSUtils.randomNegativeNumber | null + 0 | 1 | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + 0 | 1 | 0 | null + 0 | 1 | null | PBSUtils.randomNumber + 0 | 1 | null | null + 1 | 0 | PBSUtils.randomNegativeNumber | null + 1 | 0 | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + 1 | 0 | 0 | null + 1 | 0 | null | PBSUtils.randomNumber + 1 | 0 | null | null + } + + def "PBS should only drop invalid bid without discarding whole seat without debug error when request debug disabled "() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + test = DISABLED + ext.prebid.tap { + debug = DISABLED + multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] + } + } + + and: "Bid response with 2 bids" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidResponse.seatbid[0].bid << Bid.getDefaultBid(bidRequest.imp.first()) + + and: "One of the bids is invalid" + def invalidBid = bidResponse.seatbid.first().bid.first() + invalidBid.dealid = dealId + invalidBid.price = bidPrice + def validBidId = bidResponse.seatbid.first().bid.last().id + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response contains only valid bid" + assert response.seatbid?.first()?.bid*.id == [validBidId] + + and: "PBS shouldn't emit an error" + assert !response.ext?.warnings + assert !response.ext?.warnings + where: bidPrice | dealId PBSUtils.randomNegativeNumber | null @@ -257,10 +359,7 @@ class BidValidationSpec extends BaseSpec { } def "PBS should update 'adapter.generic.requests.bid_validation' metric when bid validation error appears"() { - given: "Initial 'adapter.generic.requests.bid_validation' metric value" - def initialMetricValue = getCurrentMetricValue(defaultPbsService, "adapter.generic.requests.bid_validation") - - and: "Bid request" + given: "Bid request" def bidRequest = BidRequest.defaultBidRequest and: "Set invalid bid response" diff --git a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy index 3f7682eb708..715c20c9dcb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy @@ -15,6 +15,8 @@ import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.response.auction.BidderCallType.STORED_BID_RESPONSE class DebugSpec extends BaseSpec { @@ -34,10 +36,10 @@ class DebugSpec extends BaseSpec { assert response.ext?.debug where: - debug | test - 1 | null - 1 | 0 - null | 1 + debug | test + ENABLED | null + ENABLED | DISABLED + null | ENABLED } def "PBS shouldn't return debug information when debug flag is #debug and test flag is #test"() { @@ -53,9 +55,9 @@ class DebugSpec extends BaseSpec { assert !response.ext?.debug where: - debug | test - 0 | null - null | 0 + debug | test + DISABLED | null + null | DISABLED } def "PBS should not return debug information when bidder-level setting debug.allowed = false"() { @@ -64,7 +66,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -84,7 +86,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -102,7 +104,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) @@ -132,7 +134,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) @@ -161,7 +163,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) @@ -183,7 +185,7 @@ class DebugSpec extends BaseSpec { def "PBS should use default values = true for bidder-level setting debug.allow and account-level setting debug-allowed when they are not specified"() { given: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) @@ -201,7 +203,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: debugAllowedAccount)) @@ -233,7 +235,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) @@ -278,11 +280,11 @@ class DebugSpec extends BaseSpec { assert response.ext?.debug where: - requestDebug || storedRequestDebug - 1 || 0 - 1 || 1 - 1 || null - null || 1 + requestDebug | storedRequestDebug + ENABLED | DISABLED + ENABLED | ENABLED + ENABLED | null + null | ENABLED } def "PBS AMP shouldn't return debug information when request flag is #requestDebug and stored request flag is #storedRequestDebug"() { @@ -307,12 +309,12 @@ class DebugSpec extends BaseSpec { assert !response.ext?.debug where: - requestDebug || storedRequestDebug - 0 || 1 - 0 || 0 - 0 || null - null || 0 - null || null + requestDebug | storedRequestDebug + DISABLED | ENABLED + DISABLED | DISABLED + DISABLED | null + null | DISABLED + null | null } def "PBS shouldn't populate call type when it's default bidder call"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy index 7acfb56d2ca..eeff3783811 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy @@ -23,6 +23,8 @@ import static org.mockserver.model.HttpStatusCode.PROCESSING_102 import static org.mockserver.model.HttpStatusCode.SERVICE_UNAVAILABLE_503 import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_BIDDER_UNREACHABLE @@ -219,7 +221,7 @@ class SeatNonBidSpec extends BaseSpec { assert !response.seatbid where: - debug << [1, 0, null] + debug << [ENABLED, DISABLED, null] } def "PBS shouldn't populate seatNonBid when returnAllBidStatus=false and debug=#debug and requested bidder didn't bid for any reason"() { @@ -245,7 +247,7 @@ class SeatNonBidSpec extends BaseSpec { assert !response.seatbid where: - debug << [1, 0, null] + debug << [ENABLED, DISABLED, null] } def "PBS should populate seatNonBid when bidder is rejected due to timeout"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index 71c7621eb20..8a33d00e9d2 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -25,6 +25,7 @@ import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer @@ -85,7 +86,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { static BidRequest getStoredRequestWithFloors(DistributionChannel channel = SITE) { channel == SITE ? BidRequest.defaultStoredRequest.tap { ext.prebid.floors = ExtPrebidFloors.extPrebidFloors } - : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: 1, floors: ExtPrebidFloors.extPrebidFloors))) + : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: ENABLED, floors: ExtPrebidFloors.extPrebidFloors))) } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 7eb1a351531..60cc83291f7 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -3984,7 +3984,7 @@ public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() { } @Test - public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { + public void shouldDropBidsWithInvalidPrice() { // given final Bidder bidder = mock(Bidder.class); final List bids = List.of( @@ -3998,7 +3998,35 @@ public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - final AuctionContext givenContext = givenRequestContext(bidRequest); + final AuctionContext givenContext = givenRequestContext(bidRequest).with(DebugContext.empty()); + + // when + final AuctionContext result = target.holdAuction(givenContext).result(); + + // then + assertThat(result.getBidResponse().getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1); + assertThat(givenContext.getDebugWarnings()).isEmpty(); + verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); + } + + @Test + public void shouldDropBidsWithInvalidPriceAndAddDebugWarningsWhenDebugEnabled() { + // given + final Bidder bidder = mock(Bidder.class); + final List bids = List.of( + Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), + Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), + Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), + Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); + final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); + + givenBidder("bidder", bidder, seatBid); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + final AuctionContext givenContext = givenRequestContext(bidRequest) + .with(DebugContext.of(true, false, null)); // when final AuctionContext result = target.holdAuction(givenContext).result(); From 80e6a5fcd4c57186c8b76128b383af949d9a1202 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Wed, 6 Nov 2024 14:28:08 +0100 Subject: [PATCH 117/170] PgamSsp: Add currency conversion (#3540) --- .../server/bidder/pgamssp/PgamSspBidder.java | 39 +++++++++++++++- .../config/bidder/PgamSspConfiguration.java | 4 +- .../bidder/pgamssp/PgamSspBidderTest.java | 45 ++++++++++++++++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java index ce0a5161120..a73438b3ce3 100644 --- a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java +++ b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java @@ -14,15 +14,19 @@ 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.pgamssp.PgamSspImpExt; 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.Collections; import java.util.List; @@ -35,12 +39,18 @@ public class PgamSspBidder implements Bidder { }; private static final String PUBLISHER_IMP_EXT_TYPE = "publisher"; private static final String NETWORK_IMP_EXT_TYPE = "network"; + private static final String DEFAULT_BID_CURRENCY = "USD"; private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public PgamSspBidder(String endpointUrl, JacksonMapper mapper) { + public PgamSspBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -51,7 +61,7 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { final PgamSspImpExt impExt = parseImpExt(imp); - final BidRequest modifiedBidRequest = makeRequest(request, imp, impExt); + final BidRequest modifiedBidRequest = makeRequest(request, modifyImp(imp, request), impExt); httpRequests.add(makeHttpRequest(modifiedBidRequest, imp.getId())); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -61,6 +71,31 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValues(httpRequests); } + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + final Price resolvedBidFloor = resolveBidFloor(imp, bidRequest); + return imp.toBuilder() + .bidfloor(resolvedBidFloor.getValue()) + .bidfloorcur(resolvedBidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + + return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); + } + private PgamSspImpExt parseImpExt(Imp imp) throws PreBidException { try { return mapper.mapper().convertValue(imp.getExt(), PGAMSSP_EXT_TYPE_REFERENCE).getBidder(); diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java index 1e449c950d6..7296f81625b 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.pgamssp.PgamSspBidder; +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; @@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps pgamsspBidderDeps(BidderConfigurationProperties pgamsspConfigurationProperties, + CurrencyConversionService currencyConversionService, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(pgamsspConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), currencyConversionService, mapper)) .assemble(); } } diff --git a/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java b/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java index e2b8c24ebd2..c349fd62cbe 100644 --- a/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java @@ -8,16 +8,23 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.http.HttpMethod; +import org.assertj.core.api.BDDAssertions; +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; 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.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.pgamssp.PgamSspImpExt; +import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -27,6 +34,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.prebid.server.bidder.model.BidderError.badInput; import static org.prebid.server.bidder.model.BidderError.badServerResponse; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; @@ -37,15 +47,46 @@ import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; +@ExtendWith(MockitoExtension.class) public class PgamSspBidderTest extends VertxTest { private static final String ENDPOINT_URL = "http://test-url.com"; - private final PgamSspBidder target = new PgamSspBidder(ENDPOINT_URL, jacksonMapper); + @Mock + private CurrencyConversionService currencyConversionService; + + private PgamSspBidder target; + + @BeforeEach + public void setUp() { + target = new PgamSspBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); + } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new PgamSspBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> + new PgamSspBidder("invalid_url", currencyConversionService, jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBidderCurrency() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(BDDAssertions.tuple(BigDecimal.TEN, "USD")); } @Test From 33c828b7a3b67741b097846f4bc1463a3d435913 Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Wed, 6 Nov 2024 14:28:38 +0100 Subject: [PATCH 118/170] IqZone: Add usersync (#3541) --- src/main/resources/bidder-config/iqzone.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/resources/bidder-config/iqzone.yaml b/src/main/resources/bidder-config/iqzone.yaml index ccd2f126f3f..a8292c5033a 100644 --- a/src/main/resources/bidder-config/iqzone.yaml +++ b/src/main/resources/bidder-config/iqzone.yaml @@ -13,3 +13,13 @@ adapters: - native supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: iqzone + redirect: + url: https://cs.iqzone.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + iframe: + url: https://cs.iqzone.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' From 7157ed02dc333155692084c58348ca4041cfdcad Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Fri, 8 Nov 2024 05:50:24 -0500 Subject: [PATCH 119/170] Bugfix: Make OpenRTB battr logic more strict (#3538) --- .../blocking/core/AccountConfigReader.java | 24 ++-- .../ortb2/blocking/core/RequestUpdater.java | 33 +++-- .../core/AccountConfigReaderTest.java | 44 +++--- .../blocking/core/RequestUpdaterTest.java | 118 ++++++++++++++- .../ortb2blocking/Ortb2BlockingSpec.groovy | 135 +++++++++++++++++- 5 files changed, 305 insertions(+), 49 deletions(-) diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java index a7ee0425135..47b3e3204c7 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java @@ -104,13 +104,13 @@ public Result blockedAttributesFor(BidRequest bidRequest) { final Result> bapp = blockedAttribute(BAPP_FIELD, String.class, BLOCKED_APP_FIELD, requestMediaTypes); final Result>> btype = - blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, bidRequest); + blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, BANNER_MEDIA_TYPE, bidRequest); final Result>> bannerBattr = - blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest); + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, BANNER_MEDIA_TYPE, bidRequest); final Result>> videoBattr = - blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, bidRequest); + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, VIDEO_MEDIA_TYPE, bidRequest); final Result>> audioBattr = - blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, bidRequest); + blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, AUDIO_MEDIA_TYPE, bidRequest); final Result>>> battr = mergeBlockedAttributes(bannerBattr, videoBattr, audioBattr); @@ -226,19 +226,23 @@ private Integer blockedCattaxComplementFromConfig() { private Result>> blockedAttributesForImps(String attribute, Class attributeType, String fieldName, + String attributeMediaType, BidRequest bidRequest) { final Map> attributeValues = new HashMap<>(); final List> results = new ArrayList<>(); for (final Imp imp : bidRequest.getImp()) { - final Result> attributeForImp = blockedAttribute( - attribute, attributeType, fieldName, mediaTypesFrom(imp)); - - if (attributeForImp.hasValue()) { - attributeValues.put(imp.getId(), attributeForImp.getValue()); + final Set actualMediaTypes = mediaTypesFrom(imp); + if (actualMediaTypes.contains(attributeMediaType)) { + final Result> attributeForImp = blockedAttribute( + attribute, attributeType, fieldName, actualMediaTypes); + + if (attributeForImp.hasValue()) { + attributeValues.put(imp.getId(), attributeForImp.getValue()); + } + results.add(attributeForImp); } - results.add(attributeForImp); } return Result.of( diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java index ac963b94857..78744e7c07f 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; public class RequestUpdater { @@ -99,13 +98,15 @@ private static List extractBattr(Map btype, List battr) { - final List existingBtype = banner != null ? banner.getBtype() : null; - final List existingBattr = banner != null ? banner.getBattr() : null; + if (banner == null) { + return null; + } + + final List existingBtype = banner.getBtype(); + final List existingBattr = banner.getBattr(); return CollectionUtils.isEmpty(existingBtype) || CollectionUtils.isEmpty(existingBattr) - ? Optional.ofNullable(banner) - .map(Banner::toBuilder) - .orElseGet(Banner::builder) + ? banner.toBuilder() .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btype) .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battr) .build() @@ -113,22 +114,26 @@ private static Banner updateBanner(Banner banner, List btype, List battr) { - final List existingBattr = video != null ? video.getBattr() : null; + if (video == null) { + return null; + } + + final List existingBattr = video.getBattr(); return CollectionUtils.isEmpty(existingBattr) - ? Optional.ofNullable(video) - .map(Video::toBuilder) - .orElseGet(Video::builder) + ? video.toBuilder() .battr(battr) .build() : video; } private static Audio updateAudio(Audio audio, List battr) { - final List existingBattr = audio != null ? audio.getBattr() : null; + if (audio == null) { + return null; + } + + final List existingBattr = audio.getBattr(); return CollectionUtils.isEmpty(existingBattr) - ? Optional.ofNullable(audio) - .map(Audio::toBuilder) - .orElseGet(Audio::builder) + ? audio.toBuilder() .battr(battr) .build() : audio; diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java index f6568663807..e20bf2c3dac 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java @@ -400,7 +400,7 @@ public void blockedAttributesForShouldReturnErrorWhenBlockedBannerTypeIsNotInteg final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + assertThatThrownBy(() -> reader.blockedAttributesFor(request(imp -> imp.banner(Banner.builder().build())))) .isInstanceOf(InvalidAccountConfigurationException.class) .hasMessage("blocked-banner-type field in account configuration has unexpected type. " + "Expected class java.lang.Integer"); @@ -700,17 +700,23 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri .btype(Attribute.btypeBuilder() .actionOverrides(AttributeActionOverrides.blocked(asList( ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("video")), + Conditions.of(singletonList("bidder1"), singletonList("banner")), singletonList(1)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("video")), + Conditions.of(singletonList("bidder1"), singletonList("banner")), singletonList(2)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("banner")), + Conditions.of(singletonList("bidder1"), singletonList("video")), singletonList(3)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("banner")), - singletonList(4))))) + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList(4)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("audio")), + singletonList(5)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("audio")), + singletonList(6))))) .build()) .build())); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -718,20 +724,16 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri // when and then final Map> expectedBtype = new HashMap<>(); expectedBtype.put("impId1", singletonList(1)); - expectedBtype.put("impId2", singletonList(3)); assertThat(reader .blockedAttributesFor(BidRequest.builder() .imp(asList( - Imp.builder().id("impId1").video(Video.builder().build()).build(), - Imp.builder().id("impId2").banner(Banner.builder().build()).build())) + Imp.builder().id("impId1").banner(Banner.builder().build()).build(), + Imp.builder().id("impId2").video(Video.builder().build()).build())) .build())) .isEqualTo(Result.of( BlockedAttributes.builder().btype(expectedBtype).build(), - asList( - "More than one conditions matches request. Bidder: bidder1, " + - "request media types: [video]", - "More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner]"))); + List.of("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner]"))); } @Test @@ -778,8 +780,8 @@ public void blockedAttributesForShouldReturnResultWithAllAttributesForBanner() { final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( - Result.withValue(BlockedAttributes.builder() + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").banner(Banner.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() .badv(singletonList("domain3.com")) .bcat(singletonList("cat3")) .bapp(singletonList("app3")) @@ -832,12 +834,11 @@ public void blockedAttributesForShouldReturnResultWithAllAttributesForVideo() { final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( - Result.withValue(BlockedAttributes.builder() + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").video(Video.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() .badv(singletonList("domain3.com")) .bcat(singletonList("cat3")) .bapp(singletonList("app3")) - .btype(singletonMap("impId1", singletonList(3))) .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", singletonList(3)))) .build())); } @@ -886,12 +887,11 @@ public void blockedAttributesForShouldReturnResultWithAllAttributesForAudio() { final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo( - Result.withValue(BlockedAttributes.builder() + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").audio(Audio.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() .badv(singletonList("domain3.com")) .bcat(singletonList("cat3")) .bapp(singletonList("app3")) - .btype(singletonMap("impId1", singletonList(3))) .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", singletonList(3)))) .build())); } diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java index 630c09e96d1..1f03f82edb1 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java @@ -354,7 +354,12 @@ MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) .build()); final BidRequest request = BidRequest.builder() - .imp(singletonList(Imp.builder().id("impId1").build())) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .audio(Audio.builder().build()) + .build())) .build(); // when and then @@ -373,4 +378,115 @@ MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) .build())) .build()); } + + @Test + public void shouldNotUpdateMissingBanner() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().battr(asList(3, 4)).build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingVideo() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .battr(asList(1, 2)) + .build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingAudio() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(singletonMap("impId1", asList(1, 2))) + .battr(Map.of( + MediaType.BANNER, singletonMap("impId1", asList(1, 2)), + MediaType.VIDEO, singletonMap("impId1", asList(3, 4)), + MediaType.AUDIO, singletonMap("impId1", asList(5, 6)))) + .build()); + final BidRequest request = BidRequest.builder() + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .battr(asList(1, 2)) + .build()) + .video(Video.builder().battr(asList(3, 4)).build()) + .build())) + .build()); + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy index b37cae6a067..bb644508090 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.tests.module.ortb2blocking import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration import org.prebid.server.functional.model.config.ExecutionPlan @@ -16,6 +17,8 @@ import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.Asset import org.prebid.server.functional.model.request.auction.Audio import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidderControls +import org.prebid.server.functional.model.request.auction.GenericPreferredBidder import org.prebid.server.functional.model.request.auction.Ix import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp @@ -53,12 +56,12 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor class Ortb2BlockingSpec extends ModuleBaseSpec { + private static final String WILDCARD = '*' private static final Map IX_CONFIG = ["adapters.ix.enabled" : "true", "adapters.ix.endpoint": "$networkServiceContainer.rootUri/auction".toString()] - private static final String WILDCARD = '*' private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + IX_CONFIG + - ["adapters.generic.ortb.multiformat-supported": "true"]) + ['adapter-defaults.ortb.multiformat-supported': 'false']) def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() { given: "Default bid request with proper ortb attribute" @@ -131,6 +134,134 @@ class Ortb2BlockingSpec extends ModuleBaseSpec { PBSUtils.randomNumber | BTYPE } + def "PBS shouldn't be able to send original battr ortb2 attribute when bid request imps type doesn't match with attribute type"() { + given: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain single media type" + assert bidderRequest.imp.first.mediaTypes.size() == 1 + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + bidRequest | attributeName + BidRequest.defaultVideoRequest | BANNER_BATTR + BidRequest.defaultAudioRequest | VIDEO_BATTR + BidRequest.defaultBidRequest | AUDIO_BATTR + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when preferredMediaType doesn't match with attribute type"() { + given: "Default bid request with multiply types" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.banner = Banner.defaultBanner + imp.first.video = Video.defaultVideo + imp.first.audio = Audio.defaultAudio + ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: preferredMediaType)) + } + + and: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName) + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain only preferred media type" + assert bidderRequest.imp.first.mediaTypes == [preferredMediaType] + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + preferredMediaType | attributeName + VIDEO | BANNER_BATTR + AUDIO | VIDEO_BATTR + BANNER | AUDIO_BATTR + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when account level preferredMediaType doesn't match with attribute type"() { + given: "Default bid request with multiply types" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.banner = Banner.defaultBanner + imp.first.video = Video.defaultVideo + imp.first.audio = Audio.defaultAudio + } + + and: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName).tap { + config.auction = new AccountAuctionConfig(preferredMediaType: [(GENERIC): preferredMediaType]) + } + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain only preferred media type" + assert bidderRequest.imp.first.mediaTypes == [preferredMediaType] + + and: "PBS response shouldn't contain any module errors" + assert !response?.ext?.prebid?.modules?.errors + + and: "PBS response shouldn't contain any module warning" + assert !response?.ext?.prebid?.modules?.warnings + + where: + preferredMediaType | attributeName + VIDEO | BANNER_BATTR + AUDIO | VIDEO_BATTR + BANNER | AUDIO_BATTR + } + def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() { given: "Default bid request with proper ortb attribute" def bidRequest = getBidRequestForOrtbAttribute(attributeName) From 7e41aa0be28b193b32e39a11d562e04290862708 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:07:42 +0100 Subject: [PATCH 120/170] Add useFetchDataRate for Price Floors (#3486) --- .../floors/BasicPriceFloorProcessor.java | 23 +- .../floors/PriceFloorRulesValidator.java | 10 + .../server/floors/model/PriceFloorData.java | 3 + .../model/pricefloors/PriceFloorData.groovy | 1 + .../pricefloors/PriceFloorsBaseSpec.groovy | 6 +- .../PriceFloorsCurrencySpec.groovy | 2 +- .../PriceFloorsFetchingSpec.groovy | 467 +++++++++++++----- .../floors/BasicPriceFloorProcessorTest.java | 99 +++- .../floors/PriceFloorRulesValidatorTest.java | 12 + 9 files changed, 478 insertions(+), 145 deletions(-) diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index c8163131c6e..e2983875386 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; public class BasicPriceFloorProcessor implements PriceFloorProcessor { @@ -45,6 +46,7 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; + private static final int USE_FETCH_DATA_RATE_MAX = 100; private static final int MODEL_WEIGHT_MAX_VALUE = 100; private static final int MODEL_WEIGHT_MIN_VALUE = 1; @@ -126,7 +128,7 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li final FetchResult fetchResult = floorFetcher.fetch(account); final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus); - if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) { + if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) { final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData()); return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch); } @@ -147,13 +149,20 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData); } - private static boolean shouldUseDynamicData(Account account) { - final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); - final AccountPriceFloorsConfig floorsConfig = - ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); + private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) { + final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceFloors) + .map(AccountPriceFloorsConfig::getUseDynamicData) + .map(BooleanUtils::isNotFalse) + .orElse(true); - return BooleanUtils.isNotFalse( - ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getUseDynamicData)); + final boolean shouldUseDynamicData = Optional.ofNullable(fetchResult.getRulesData()) + .map(PriceFloorData::getUseFetchDataRate) + .map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate) + .orElse(true); + + return isUsingDynamicDataAllowed && shouldUseDynamicData; } private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java index b976ea69c97..a5f97299340 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -17,6 +17,8 @@ public class PriceFloorRulesValidator { private static final int MODEL_WEIGHT_MIN_VALUE = 1; private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; + private static final int USE_FETCH_DATA_RATE_MIN = 0; + private static final int USE_FETCH_DATA_RATE_MAX = 100; private PriceFloorRulesValidator() { } @@ -48,6 +50,14 @@ public static void validateRulesData(PriceFloorData priceFloorData, Integer maxR "Price floor data skipRate must be in range(0-100), but was " + dataSkipRate); } + final Integer useFetchDataRate = priceFloorData.getUseFetchDataRate(); + if (useFetchDataRate != null + && (useFetchDataRate < USE_FETCH_DATA_RATE_MIN || useFetchDataRate > USE_FETCH_DATA_RATE_MAX)) { + + throw new PreBidException( + "Price floor data useFetchDataRate must be in range(0-100), but was " + useFetchDataRate); + } + if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) { throw new PreBidException("Price floor rules should contain at least one model group"); } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java index bdca465af36..4604ed91892 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java @@ -18,6 +18,9 @@ public class PriceFloorData { @JsonProperty("skipRate") Integer skipRate; + @JsonProperty("useFetchDataRate") + Integer useFetchDataRate; + @JsonProperty("floorsSchemaVersion") String floorsSchemaVersion; diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy index 431890c371d..a9d913eccd1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy @@ -16,6 +16,7 @@ class PriceFloorData implements ResponseModel { String floorProvider Currency currency Integer skipRate + Integer useFetchDataRate String floorsSchemaVersion Integer modelTimestamp List modelGroups diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index 8a33d00e9d2..fba9a5b44d5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -37,14 +37,14 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { public static final Map FLOORS_CONFIG = ["price-floors.enabled" : "true", "settings.default-account-config": encode(defaultAccountConfigSettings)] - protected static final String basicFetchUrl = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT - protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer) + protected static final String BASIC_FETCH_URL = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT protected static final int MAX_MODEL_WEIGHT = 100 private static final int DEFAULT_MODEL_WEIGHT = 1 private static final int CURRENCY_CONVERSION_PRECISION = 3 private static final int FLOOR_VALUE_PRECISION = 4 + protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer) protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG) def setupSpec() { @@ -69,7 +69,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { protected static Account getAccountWithEnabledFetch(String accountId) { def priceFloors = new AccountPriceFloorsConfig(enabled: true, - fetch: new PriceFloorsFetch(url: basicFetchUrl + accountId, enabled: true)) + fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + accountId, enabled: true)) def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(priceFloors: priceFloors)) new Account(uuid: accountId, config: accountConfig) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index 581a71644a5..786a70a6b86 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -243,7 +243,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = priceFloors config.auction.priceFloorsSnakeCase = new AccountPriceFloorsConfig(enabled: true, - fetch: new PriceFloorsFetch(url: basicFetchUrl + bidRequest.accountId, enabled: priceFloorsSnakeCase)) + fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + bidRequest.accountId, enabled: priceFloorsSnakeCase)) } accountDao.save(account) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 22e5b27a04f..f282d69f600 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -20,24 +20,28 @@ import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH +import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Location.REQUEST import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { - private static final int MAX_ENFORCE_FLOORS_RATE = 100 + private static final int ENFORCE_FLOORS_RATE_MAX = 100 private static final int DEFAULT_MAX_AGE_SEC = 600 private static final int DEFAULT_PERIOD_SEC = 300 - private static final int MIN_TIMEOUT_MS = 10 - private static final int MAX_TIMEOUT_MS = 10000 - private static final int MIN_SKIP_RATE = 0 - private static final int MAX_SKIP_RATE = 100 - private static final int MIN_DEFAULT_FLOOR_VALUE = 0 - private static final int MIN_FLOOR_MIN = 0 + private static final int TIMEOUT_MS_MIN = 10 + private static final int TIMEOUT_MS_MAX = 10000 + private static final int SKIP_RATE_MIN = 0 + private static final int SKIP_RATE_MAX = 100 + private static final int USE_FETCH_DATA_RATE_MIN = 0 + private static final int USE_FETCH_DATA_RATE_MAX = 100 + private static final int DEFAULT_FLOOR_VALUE_MIN = 0 + private static final int FLOOR_MIN = 0 private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure" @@ -50,7 +54,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch and fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "PBS fetch rules from floors provider" @@ -60,7 +64,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS should signal bids" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -75,7 +79,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch and fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.enabled = accountConfigEnabled } accountDao.save(account) @@ -87,7 +91,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should no fetching, no signaling, no enforcing" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 0 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 0 def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert !bidderRequest.imp[0].bidFloor @@ -105,7 +109,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, without fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.url = null } accountDao.save(account) @@ -115,9 +119,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "PBS should log error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, bidRequest.site.publisher.id) + def floorsLogs = getLogsByText(logs, bidRequest.accountId) assert floorsLogs.size() == 1 - assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.site.publisher.id") + assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.accountId") and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() @@ -132,8 +136,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxAgeSec in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch = fetchConfig(bidRequest.app.publisher.id, DEFAULT_MAX_AGE_SEC - 1) + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch = fetchConfig(bidRequest.accountId, DEFAULT_MAX_AGE_SEC - 1) } accountDao.save(account) @@ -142,14 +146,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() where: - fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSec: max) }, - { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSecSnakeCase: max) }] + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSec: max) }, + { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSecSnakeCase: max) }] } def "PBS should validate fetch.period-sec from account config"() { @@ -157,7 +161,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, periodSec in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch = fetchConfig(DEFAULT_PERIOD_SEC, defaultAccountConfigSettings.auction.priceFloors.fetch.maxAgeSec) } @@ -168,7 +172,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -185,7 +189,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxFileSizeKb in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.maxFileSizeKb = PBSUtils.randomNegativeNumber } accountDao.save(account) @@ -195,7 +199,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -206,7 +210,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxRules in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.maxRules = PBSUtils.randomNegativeNumber } accountDao.save(account) @@ -216,7 +220,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -227,8 +231,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, timeoutMs in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch = fetchConfig(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS) + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch = fetchConfig(TIMEOUT_MS_MIN, TIMEOUT_MS_MAX) } accountDao.save(account) @@ -237,7 +241,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -254,7 +258,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, enforceFloorsRate in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.tap { it.enforceFloorsRate = enforceFloorsRate it.enforceFloorsRateSnakeCase = enforceFloorsRateSnakeCase @@ -267,7 +271,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -275,9 +279,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { where: enforceFloorsRate | enforceFloorsRateSnakeCase PBSUtils.randomNegativeNumber | null - MAX_ENFORCE_FLOORS_RATE + 1 | null + ENFORCE_FLOORS_RATE_MAX + 1 | null null | PBSUtils.randomNegativeNumber - null | MAX_ENFORCE_FLOORS_RATE + 1 + null | ENFORCE_FLOORS_RATE_MAX + 1 } def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() { @@ -287,7 +291,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = true } accountDao.save(account) @@ -296,7 +300,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 } def "PBS should process floors from request when price-floors.fetch.enabled = false in account config"() { @@ -304,7 +308,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with fetch.enabled, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch = fetch } accountDao.save(account) @@ -319,17 +323,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "PBS should not fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 0 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor where: - fetch << [new PriceFloorsFetch(enabled: false, url: basicFetchUrl), new PriceFloorsFetch(url: basicFetchUrl)] + fetch << [new PriceFloorsFetch(enabled: false, url: BASIC_FETCH_URL), new PriceFloorsFetch(url: BASIC_FETCH_URL)] } - def "PBS should fetch data from provider when use-dynamic-data = true"() { + def "PBS should fetch data from provider when use-dynamic-data enabled"() { given: "Pbs with PF configuration with useDynamicData" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { auction.priceFloors.tap { @@ -346,7 +350,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.useDynamicData = accountUseDynamicData config.auction.priceFloors.useDynamicDataSnakeCase = accountUseDynamicDataSnakeCase } @@ -357,13 +361,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS cache rules and processes auction request" cacheFloorsProviderRules(bidRequest, floorValue, pbsService) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -381,6 +385,211 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { null | null | null | true } + def "PBS should fetch data from provider when use-dynamic-data enabled and useFetchDataRate at max value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MAX + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + imp[0].bidFloor == floorValue + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] + imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue + imp[0].ext?.prebid?.floors?.floorValue == floorValue + + ext?.prebid?.floors?.location == FETCH + ext?.prebid?.floors?.fetchStatus == SUCCESS + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + + ext?.prebid?.floors?.skipRate == floorsResponse.skipRate + ext?.prebid?.floors?.data == floorsResponse + } + } + + def "PBS shouldn't fetch data from provider when use-dynamic-data disabled and useFetchDataRate at max value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with disabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = false + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MAX + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request shouldn't contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + !ext?.prebid?.floors?.floorProvider + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == SUCCESS + } + } + + def "PBS shouldn't fetch data from provider when use-dynamic-data enabled and useFetchDataRate at min value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MIN + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request shouldn't contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + !ext?.prebid?.floors?.floorProvider + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == SUCCESS + } + } + + def "PBS should log error and increase metrics when useFetchDataRate have invalid value"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch and fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = accounntUseFetchDataRate + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "metric should be updated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[FETCH_FAILURE_METRIC] == 1 + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId") + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("reason : Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.floorProvider + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == ERROR + } + + where: + accounntUseFetchDataRate << [PBSUtils.getRandomNegativeNumber(-USE_FETCH_DATA_RATE_MAX, USE_FETCH_DATA_RATE_MIN), + PBSUtils.getRandomNumber(USE_FETCH_DATA_RATE_MAX + 1) + ] + } + def "PBS should process floors from request when use-dynamic-data = false"() { given: "Pbs with PF configuration with useDynamicData" def defaultAccountConfigSettings = defaultAccountConfigSettings.tap { @@ -393,7 +602,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with fetch.enabled, fetch.url, useDynamicData in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.useDynamicData = accountUseDynamicData } accountDao.save(account) @@ -405,7 +614,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -429,7 +638,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -450,10 +659,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to request for " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to request for " + "account $accountId, provider respond with status 400") and: "Floors validation failure cannot reject the entire auction" @@ -471,7 +680,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -490,10 +699,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " + "response for account $accountId, cause: DecodeException: Failed to decode") and: "Floors validation failure cannot reject the entire auction" @@ -511,7 +720,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -529,10 +738,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " + "response for account $accountId, response body can not be empty" as String) and: "Floors validation failure cannot reject the entire auction" @@ -550,7 +759,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -571,10 +780,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules should contain " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules should contain " + "at least one model group " as String) and: "Floors validation failure cannot reject the entire auction" @@ -592,7 +801,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -613,10 +822,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules values can't " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules values can't " + "be null or empty, but were null" as String) and: "Floors validation failure cannot reject the entire auction" @@ -634,7 +843,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def maxRules = 1 def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.fetch = fetchConfig(accountId, maxRules) @@ -658,18 +867,18 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules number " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules number " + "2 exceeded its maximum number $maxRules") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRules: max) }, - { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRulesSnakeCase: max) }] + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRules: max) }, + { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRulesSnakeCase: max) }] } def "PBS should log error and increase #FETCH_FAILURE_METRIC when fetch request exceeds fetch.timeout-ms"() { @@ -686,7 +895,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -704,9 +913,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = pbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$basicFetchUrl$accountId', " + + assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$BASIC_FETCH_URL$accountId', " + "account $accountId exceeded") and: "Floors validation failure cannot reject the entire auction" @@ -724,7 +933,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with maxFileSizeKb in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def maxSize = PBSUtils.getRandomNumber(1, 5) def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.fetch = fetchConfig(accountId, maxSize) @@ -747,35 +956,36 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Response size " + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Response size " + "$responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKb: maxKbSize) }, - { String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }] + fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKb: maxKbSize) }, + { String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }] } def "PBS should prefer data from stored request when request doesn't contain floors data"() { given: "Default BidRequest with storedRequest" + def storedRequestId = PBSUtils.randomNumber as String def bidRequest = request.tap { - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) } and: "Default stored request with floors" def storedRequestModel = bidRequestWithFloors and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel) + def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel) storedRequestDao.save(storedRequest) and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(accountId).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -804,9 +1014,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } where: - request | accountId | bidRequestWithFloors - BidRequest.defaultBidRequest | request.site.publisher.id | bidRequestWithFloors - BidRequest.getDefaultBidRequest(APP) | request.app.publisher.id | getBidRequestWithFloors(APP) + request | bidRequestWithFloors + BidRequest.defaultBidRequest | getBidRequestWithFloors(SITE) + BidRequest.getDefaultBidRequest(APP) | getBidRequestWithFloors(APP) } def "PBS should prefer data from request when fetch is disabled in account config"() { @@ -814,7 +1024,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -850,7 +1060,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest - and: "Default stored request with floors " + and: "Default stored request with floors" def ampStoredRequest = storedRequestWithFloors def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -887,8 +1097,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def "PBS should prefer data from floors provider when floors data is defined in both request and stored request"() { given: "BidRequest with storedRequest" + def storedRequestId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithFloors.tap { - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) ext.prebid.floors.floorMin = FLOOR_MIN } @@ -896,11 +1107,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def storedRequestModel = bidRequestWithFloors and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel) + def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel) storedRequestDao.save(storedRequest) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -908,13 +1119,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS cache rules and processes auction request" cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService) then: "Bidder request should contain floors data from floors provider" - def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last verifyAll(bidderRequest) { imp[0].bidFloor == floorValue imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency @@ -987,20 +1198,20 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider #description response" - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) then: "PBS should cache data from data provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS should periodically fetch data from data provider" - PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.app.publisher.id) > 1 }, 7000, 3000) + PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.accountId) > 1 }, 7000, 3000) where: description | floorsResponse @@ -1020,7 +1231,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider #description response" @@ -1028,7 +1239,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) @@ -1066,14 +1277,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def "PBS should validate rules from request when floorMin from request is invalid"() { given: "Default BidRequest with floorMin" def floorValue = PBSUtils.randomFloorValue - def invalidFloorMin = MIN_FLOOR_MIN - 1 + def invalidFloorMin = FLOOR_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue ext.prebid.floors.floorMin = invalidFloorMin } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1101,7 +1312,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1129,7 +1340,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1161,7 +1372,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1237,7 +1448,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1259,7 +1470,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be in range(0-100), but was $invalidSkipRate "] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when data skipRate from request is invalid"() { @@ -1277,7 +1488,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1299,7 +1510,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be in range(0-100), but was $invalidSkipRate "] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when modelGroup skipRate from request is invalid"() { @@ -1317,7 +1528,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1339,24 +1550,24 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be in range(0-100), but was $invalidSkipRate "] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should validate rules from request when default floor value from request is invalid"() { given: "Default BidRequest with default floor value" def floorValue = PBSUtils.randomFloorValue - def invalidDefaultFloorValue = MIN_DEFAULT_FLOOR_VALUE - 1 + def invalidDefaultFloorValue = DEFAULT_FLOOR_VALUE_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] - ext.prebid.floors.data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + ext.prebid.floors.data.modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1384,7 +1595,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1436,7 +1647,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1470,10 +1681,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" + " must be in range(1-100), but was $invalidModelWeight") and: "Floors validation failure cannot reject the entire auction" @@ -1494,7 +1705,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1529,17 +1740,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor data skipRate" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor data skipRate" + " must be in range(0-100), but was $invalidSkipRate") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when modelGroup skipRate from floors provider is invalid"() { @@ -1553,7 +1764,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1588,17 +1799,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + " must be in range(0-100), but was $invalidSkipRate") and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when default floor value from floors provider is invalid"() { @@ -1612,19 +1823,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1 + def invalidDefaultFloor = DEFAULT_FLOOR_VALUE_MIN - 1 def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups << ModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].defaultFloor = invalidDefaultFloor modelGroups.last().values = [(rule): floorValue] - modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN } floorsProvider.setResponse(accountId, floorsResponse) @@ -1647,10 +1858,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId") + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId") assert floorsLogs.size() == 1 assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup default" + + "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup default" + " must be positive float, but was $invalidDefaultFloor") and: "Floors validation failure cannot reject the entire auction" @@ -1662,7 +1873,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -1672,7 +1883,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { modelGroups[0].currency = modelGroupCurrency currency = dataCurrency } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) @@ -1698,7 +1909,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1718,7 +1929,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "PBS log should not contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 0 and: "Bidder request should contain floors data from floors provider" @@ -1732,7 +1943,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1768,7 +1979,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1806,7 +2017,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 0990d97b5aa..8cc02e64a4a 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -118,9 +118,12 @@ public void shouldUseFloorsDataFromProviderIfPresent() { } @Test - public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { + public void shouldUseFloorsFromProviderIfUseDynamicDataAndUseFetchDataRateAreAbsent() { // given - final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com")); + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(null)); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when @@ -142,9 +145,65 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { } @Test - public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() { + public void shouldUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs100() { // given - final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com")); + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(100)); + + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), null), + givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)), + "bidder", + new ArrayList<>(), + new ArrayList<>()); + + // then + assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors + .enabled(true) + .skipped(false) + .floorProvider("provider.com") + .data(providerFloorsData) + .fetchStatus(FetchStatus.success) + .location(PriceFloorLocation.fetch))); + } + + @Test + public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs0() { + // given + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(0)); + + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), null), + givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)), + "bidder", + new ArrayList<>(), + new ArrayList<>()); + + // then + final PriceFloorRules actualRules = extractFloors(result); + assertThat(actualRules) + .extracting(PriceFloorRules::getFetchStatus) + .isEqualTo(FetchStatus.success); + assertThat(actualRules) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); + } + + @Test + public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrueAndUseFetchDataRateIsAbsent() { + // given + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(null)); given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when @@ -167,9 +226,37 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() { } @Test - public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() { + public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIsAbsent() { // given - final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com")); + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(null)); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); + + // when + final BidRequest result = target.enrichWithPriceFloors( + givenBidRequest(identity(), null), + givenAccount(floorsConfig -> floorsConfig.useDynamicData(false)), + "bidder", + new ArrayList<>(), + new ArrayList<>()); + + // then + final PriceFloorRules actualRules = extractFloors(result); + assertThat(actualRules) + .extracting(PriceFloorRules::getFetchStatus) + .isEqualTo(FetchStatus.success); + assertThat(actualRules) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); + } + + @Test + public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIs100() { + // given + final PriceFloorData providerFloorsData = givenFloorData(floors -> floors + .floorProvider("provider.com") + .useFetchDataRate(100)); given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when diff --git a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java index 77c4f56123c..5516bb5c9e1 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java @@ -62,6 +62,18 @@ public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() { .withMessage("Price floor data skipRate must be in range(0-100), but was -1"); } + @Test + public void validateShouldThrowExceptionOnInvalidUseFetchDataRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData( + dataBuilder -> dataBuilder.useFetchDataRate(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) + .withMessage("Price floor data useFetchDataRate must be in range(0-100), but was -1"); + } + @Test public void validateShouldThrowExceptionOnAbsentDataModelGroups() { // given From 84b3a708fe357a564f27b8bc25aef1a42bce023c Mon Sep 17 00:00:00 2001 From: EvgeniiMunin <35193823+EvgeniiMunin@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:44:13 +0100 Subject: [PATCH 121/170] Greenbids RTD: Add Module (#3242) --- extra/bundle/pom.xml | 5 + .../modules/greenbids-real-time-data/pom.xml | 37 ++ .../src/lombok.config | 1 + .../data/config/DatabaseReaderFactory.java | 55 ++ .../GreenbidsRealTimeDataConfiguration.java | 134 +++++ .../config/GreenbidsRealTimeDataModule.java | 29 ++ .../GreenbidsRealTimeDataProperties.java | 21 + .../real/time/data/core/FilterService.java | 123 +++++ .../core/GreenbidsInferenceDataService.java | 184 +++++++ .../data/core/GreenbidsInvocationService.java | 120 +++++ .../time/data/core/GreenbidsUserAgent.java | 67 +++ .../real/time/data/core/ModelCache.java | 96 ++++ .../real/time/data/core/OnnxModelRunner.java | 24 + .../data/core/OnnxModelRunnerFactory.java | 10 + .../core/OnnxModelRunnerWithThresholds.java | 31 ++ .../real/time/data/core/ThresholdCache.java | 102 ++++ .../core/ThrottlingThresholdsFactory.java | 15 + .../real/time/data/model/data/Partner.java | 34 ++ .../data/model/data/ThrottlingMessage.java | 25 + .../model/filter/ThrottlingThresholds.java | 13 + .../data/model/result/AnalyticsResult.java | 18 + .../result/GreenbidsInvocationResult.java | 15 + ...alTimeDataProcessedAuctionRequestHook.java | 210 ++++++++ .../data/v1/model/InvocationResultImpl.java | 36 ++ .../data/v1/model/analytics/ActivityImpl.java | 19 + .../v1/model/analytics/AppliedToImpl.java | 24 + .../data/v1/model/analytics/ResultImpl.java | 18 + .../data/v1/model/analytics/TagsImpl.java | 15 + .../time/data/core/FilterServiceTest.java | 177 +++++++ .../GreenbidsInferenceDataServiceTest.java | 165 ++++++ .../core/GreenbidsInvocationServiceTest.java | 126 +++++ .../data/core/GreenbidsUserAgentTest.java | 59 +++ .../real/time/data/core/ModelCacheTest.java | 191 +++++++ .../time/data/core/OnnxModelRunnerTest.java | 73 +++ .../time/data/core/ThresholdCacheTest.java | 198 ++++++++ .../data/util/TestBidRequestProvider.java | 93 ++++ ...meDataProcessedAuctionRequestHookTest.java | 473 ++++++++++++++++++ .../resources/models_pbuid=test-pbuid.onnx | Bin 0 -> 4212 bytes .../thresholds_pbuid=test-pbuid.json | 14 + extra/modules/pom.xml | 1 + .../greenbids/GreenbidsAnalyticsReporter.java | 102 +++- .../greenbids/model/ExplorationResult.java | 18 + .../greenbids/model/GreenbidsAdUnit.java | 3 + .../greenbids/model/Ortb2ImpExtResult.java | 11 + .../greenbids/model/Ortb2ImpResult.java | 9 + .../GreenbidsAnalyticsReporterTest.java | 229 ++++++++- 46 files changed, 3414 insertions(+), 9 deletions(-) create mode 100644 extra/modules/greenbids-real-time-data/pom.xml create mode 100644 extra/modules/greenbids-real-time-data/src/lombok.config create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java create mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java create mode 100644 extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx create mode 100644 extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json create mode 100644 src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java create mode 100644 src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java create mode 100644 src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 5f17d237be8..b6dfbac6ea8 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -45,6 +45,11 @@ pb-response-correction ${project.version} + + org.prebid.server.hooks.modules + greenbids-real-time-data + ${project.version} + diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml new file mode 100644 index 00000000000..2d6bf82383a --- /dev/null +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + org.prebid.server.hooks.modules + all-modules + 3.15.0-SNAPSHOT + + + greenbids-real-time-data + + greenbids-real-time-data + Greenbids Real Time Data + + + + com.github.ua-parser + uap-java + 1.6.1 + + + + com.microsoft.onnxruntime + onnxruntime + 1.16.1 + + + + com.google.cloud + google-cloud-storage + 2.41.0 + + + + diff --git a/extra/modules/greenbids-real-time-data/src/lombok.config b/extra/modules/greenbids-real-time-data/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java new file mode 100644 index 00000000000..a40c98ebb25 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java @@ -0,0 +1,55 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import com.maxmind.geoip2.DatabaseReader; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.vertx.Initializable; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; + +public class DatabaseReaderFactory implements Initializable { + + private final String geoLiteCountryUrl; + + private final Vertx vertx; + + private final AtomicReference databaseReaderRef = new AtomicReference<>(); + + public DatabaseReaderFactory(String geoLitCountryUrl, Vertx vertx) { + this.geoLiteCountryUrl = geoLitCountryUrl; + this.vertx = vertx; + } + + @Override + public void initialize(Promise initializePromise) { + + vertx.executeBlocking(() -> { + try { + final URL url = new URL(geoLiteCountryUrl); + final Path databasePath = Files.createTempFile("GeoLite2-Country", ".mmdb"); + + try (InputStream inputStream = url.openStream(); + FileOutputStream outputStream = new FileOutputStream(databasePath.toFile())) { + inputStream.transferTo(outputStream); + } + + databaseReaderRef.set(new DatabaseReader.Builder(databasePath.toFile()).build()); + } catch (IOException e) { + throw new PreBidException("Failed to initialize DatabaseReader from URL", e); + } + return null; + }).mapEmpty() + .onComplete(initializePromise); + } + + public DatabaseReader getDatabaseReader() { + return databaseReaderRef.get(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java new file mode 100644 index 00000000000..959352d1908 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java @@ -0,0 +1,134 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import io.vertx.core.Vertx; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.GreenbidsRealTimeDataProcessedAuctionRequestHook; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@ConditionalOnProperty(prefix = "hooks." + GreenbidsRealTimeDataModule.CODE, name = "enabled", havingValue = "true") +@Configuration +@EnableConfigurationProperties(GreenbidsRealTimeDataProperties.class) +public class GreenbidsRealTimeDataConfiguration { + + @Bean + DatabaseReaderFactory databaseReaderFactory(GreenbidsRealTimeDataProperties properties, Vertx vertx) { + return new DatabaseReaderFactory(properties.getGeoLiteCountryPath(), vertx); + } + + @Bean + GreenbidsInferenceDataService greenbidsInferenceDataService(DatabaseReaderFactory databaseReaderFactory) { + return new GreenbidsInferenceDataService( + databaseReaderFactory, ObjectMapperProvider.mapper()); + } + + @Bean + GreenbidsRealTimeDataModule greenbidsRealTimeDataModule( + FilterService filterService, + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds, + GreenbidsInferenceDataService greenbidsInferenceDataService, + GreenbidsInvocationService greenbidsInvocationService) { + + return new GreenbidsRealTimeDataModule(List.of( + new GreenbidsRealTimeDataProcessedAuctionRequestHook( + ObjectMapperProvider.mapper(), + filterService, + onnxModelRunnerWithThresholds, + greenbidsInferenceDataService, + greenbidsInvocationService))); + } + + @Bean + FilterService filterService() { + return new FilterService(); + } + + @Bean + Storage storage(GreenbidsRealTimeDataProperties properties) { + return StorageOptions.newBuilder() + .setProjectId(properties.getGoogleCloudGreenbidsProject()).build().getService(); + } + + @Bean + OnnxModelRunnerFactory onnxModelRunnerFactory() { + return new OnnxModelRunnerFactory(); + } + + @Bean + ThrottlingThresholdsFactory throttlingThresholdsFactory() { + return new ThrottlingThresholdsFactory(); + } + + @Bean + ModelCache modelCache( + GreenbidsRealTimeDataProperties properties, + Vertx vertx, + Storage storage, + OnnxModelRunnerFactory onnxModelRunnerFactory) { + + final Cache modelCacheWithExpiration = Caffeine.newBuilder() + .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES) + .build(); + + return new ModelCache( + storage, + properties.getGcsBucketName(), + modelCacheWithExpiration, + properties.getOnnxModelCacheKeyPrefix(), + vertx, + onnxModelRunnerFactory); + } + + @Bean + ThresholdCache thresholdCache( + GreenbidsRealTimeDataProperties properties, + Vertx vertx, + Storage storage, + ThrottlingThresholdsFactory throttlingThresholdsFactory) { + + final Cache thresholdsCacheWithExpiration = Caffeine.newBuilder() + .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES) + .build(); + + return new ThresholdCache( + storage, + properties.getGcsBucketName(), + ObjectMapperProvider.mapper(), + thresholdsCacheWithExpiration, + properties.getThresholdsCacheKeyPrefix(), + vertx, + throttlingThresholdsFactory); + } + + @Bean + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds( + ModelCache modelCache, + ThresholdCache thresholdCache) { + + return new OnnxModelRunnerWithThresholds(modelCache, thresholdCache); + } + + @Bean + GreenbidsInvocationService greenbidsInvocationService() { + return new GreenbidsInvocationService(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java new file mode 100644 index 00000000000..b2e5bdcfeb8 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.List; + +public class GreenbidsRealTimeDataModule implements Module { + + public static final String CODE = "greenbids-real-time-data"; + + private final List> hooks; + + public GreenbidsRealTimeDataModule(List> hooks) { + this.hooks = hooks; + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java new file mode 100644 index 00000000000..86736a6011f --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "hooks.modules." + GreenbidsRealTimeDataModule.CODE) +@Data +public class GreenbidsRealTimeDataProperties { + + String googleCloudGreenbidsProject; + + String geoLiteCountryPath; + + String gcsBucketName; + + Integer cacheExpirationMinutes; + + String onnxModelCacheKeyPrefix; + + String thresholdsCacheKeyPrefix; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java new file mode 100644 index 00000000000..094c2d18df1 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java @@ -0,0 +1,123 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OnnxValue; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class FilterService { + + public Map> filterBidders( + OnnxModelRunner onnxModelRunner, + List throttlingMessages, + Double threshold) { + + final OrtSession.Result results; + try { + final String[][] throttlingInferenceRows = convertToArray(throttlingMessages); + results = onnxModelRunner.runModel(throttlingInferenceRows); + return processModelResults(results, throttlingMessages, threshold); + } catch (OrtException e) { + throw new PreBidException("Exception during model inference: ", e); + } + } + + private static String[][] convertToArray(List messages) { + return messages.stream() + .map(message -> new String[]{ + message.getBrowser(), + message.getBidder(), + message.getAdUnitCode(), + message.getCountry(), + message.getHostname(), + message.getDevice(), + message.getHourBucket(), + message.getMinuteQuadrant()}) + .toArray(String[][]::new); + } + + private Map> processModelResults( + OrtSession.Result results, + List throttlingMessages, + Double threshold) { + + validateThrottlingMessages(throttlingMessages); + + return StreamSupport.stream(results.spliterator(), false) + .peek(FilterService::validateOnnxTensor) + .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities")) + .map(Map.Entry::getValue) + .map(OnnxTensor.class::cast) + .peek(tensor -> validateTensorSize(tensor, throttlingMessages.size())) + .map(tensor -> extractAndProcessProbabilities(tensor, throttlingMessages, threshold)) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static void validateThrottlingMessages(List throttlingMessages) { + if (throttlingMessages == null || CollectionUtils.isEmpty(throttlingMessages)) { + throw new PreBidException("throttlingMessages cannot be null or empty"); + } + } + + private static void validateOnnxTensor(Map.Entry onnxItem) { + if (!(onnxItem.getValue() instanceof OnnxTensor)) { + throw new PreBidException("Expected OnnxTensor for 'probabilities', but found: " + + onnxItem.getValue().getClass().getName()); + } + } + + private static void validateTensorSize(OnnxTensor tensor, int expectedSize) { + final long[] tensorShape = tensor.getInfo().getShape(); + if (tensorShape.length == 0 || tensorShape[0] != expectedSize) { + throw new PreBidException("Mismatch between tensor size and throttlingMessages size"); + } + } + + private Map> extractAndProcessProbabilities( + OnnxTensor tensor, + List throttlingMessages, + Double threshold) { + + try { + final float[][] probabilities = extractProbabilitiesValues(tensor); + return processProbabilities(probabilities, throttlingMessages, threshold); + } catch (OrtException e) { + throw new PreBidException("Exception when extracting proba from OnnxTensor: ", e); + } + } + + private float[][] extractProbabilitiesValues(OnnxTensor tensor) throws OrtException { + return (float[][]) tensor.getValue(); + } + + private Map> processProbabilities( + float[][] probabilities, + List throttlingMessages, + Double threshold) { + + final Map> result = new HashMap<>(); + + for (int i = 0; i < probabilities.length; i++) { + final ThrottlingMessage message = throttlingMessages.get(i); + final String impId = message.getAdUnitCode(); + final String bidder = message.getBidder(); + final boolean isKeptInAuction = probabilities[i][1] > threshold; + result.computeIfAbsent(impId, k -> new HashMap<>()).put(bidder, isKeptInAuction); + } + + return result; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java new file mode 100644 index 00000000000..3bd3e37b859 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java @@ -0,0 +1,184 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.Country; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; + +import java.io.IOException; +import java.net.InetAddress; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class GreenbidsInferenceDataService { + + private final DatabaseReaderFactory databaseReaderFactory; + + private final ObjectMapper mapper; + + public GreenbidsInferenceDataService(DatabaseReaderFactory dbReaderFactory, ObjectMapper mapper) { + this.databaseReaderFactory = Objects.requireNonNull(dbReaderFactory); + this.mapper = Objects.requireNonNull(mapper); + } + + public List extractThrottlingMessagesFromBidRequest(BidRequest bidRequest) { + final GreenbidsUserAgent userAgent = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getUa) + .map(GreenbidsUserAgent::new) + .orElse(null); + + return extractThrottlingMessages(bidRequest, userAgent); + } + + private List extractThrottlingMessages( + BidRequest bidRequest, + GreenbidsUserAgent greenbidsUserAgent) { + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer hourBucket = timestamp.getHour(); + final Integer minuteQuadrant = (timestamp.getMinute() / 15) + 1; + + final String hostname = bidRequest.getSite().getDomain(); + final List imps = bidRequest.getImp(); + + return imps.stream() + .map(imp -> extractMessagesForImp( + imp, + bidRequest, + greenbidsUserAgent, + hostname, + hourBucket, + minuteQuadrant)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private List extractMessagesForImp( + Imp imp, + BidRequest bidRequest, + GreenbidsUserAgent greenbidsUserAgent, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final String impId = imp.getId(); + final ObjectNode impExt = imp.getExt(); + final JsonNode bidderNode = extImpPrebid(impExt.get("prebid")).getBidder(); + final String ip = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getIp) + .orElse(null); + final String countryFromIp = getCountry(ip); + return createThrottlingMessages( + bidderNode, + impId, + greenbidsUserAgent, + countryFromIp, + hostname, + hourBucket, + minuteQuadrant); + } + + private String getCountry(String ip) { + if (ip == null) { + return null; + } + + final DatabaseReader databaseReader = databaseReaderFactory.getDatabaseReader(); + try { + final InetAddress inetAddress = InetAddress.getByName(ip); + final CountryResponse response = databaseReader.country(inetAddress); + final Country country = response.getCountry(); + return country.getName(); + } catch (IOException | GeoIp2Exception e) { + throw new PreBidException("Failed to fetch country from geoLite DB", e); + } + } + + private List createThrottlingMessages( + JsonNode bidderNode, + String impId, + GreenbidsUserAgent greenbidsUserAgent, + String countryFromIp, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final List throttlingImpMessages = new ArrayList<>(); + + if (!bidderNode.isObject()) { + return throttlingImpMessages; + } + + final ObjectNode bidders = (ObjectNode) bidderNode; + final Iterator fieldNames = bidders.fieldNames(); + while (fieldNames.hasNext()) { + final String bidderName = fieldNames.next(); + throttlingImpMessages.add(buildThrottlingMessage( + bidderName, + impId, + greenbidsUserAgent, + countryFromIp, + hostname, + hourBucket, + minuteQuadrant)); + } + + return throttlingImpMessages; + } + + private ThrottlingMessage buildThrottlingMessage( + String bidderName, + String impId, + GreenbidsUserAgent greenbidsUserAgent, + String countryFromIp, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final String browser = Optional.ofNullable(greenbidsUserAgent) + .map(GreenbidsUserAgent::getBrowser) + .orElse(StringUtils.EMPTY); + + final String device = Optional.ofNullable(greenbidsUserAgent) + .map(GreenbidsUserAgent::getDevice) + .orElse(StringUtils.EMPTY); + + return ThrottlingMessage.builder() + .browser(browser) + .bidder(StringUtils.defaultString(bidderName)) + .adUnitCode(StringUtils.defaultString(impId)) + .country(StringUtils.defaultString(countryFromIp)) + .hostname(StringUtils.defaultString(hostname)) + .device(device) + .hourBucket(StringUtils.defaultString(hourBucket.toString())) + .minuteQuadrant(StringUtils.defaultString(minuteQuadrant.toString())) + .build(); + } + + private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { + try { + return mapper.treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java new file mode 100644 index 00000000000..67d42d47bc2 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java @@ -0,0 +1,120 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public class GreenbidsInvocationService { + + private static final int RANGE_16_BIT_INTEGER_DIVISION_BASIS = 0x10000; + + public GreenbidsInvocationResult createGreenbidsInvocationResult( + Partner partner, + BidRequest bidRequest, + Map> impsBiddersFilterMap) { + + final String greenbidsId = UUID.randomUUID().toString(); + final boolean isExploration = isExploration(partner, greenbidsId); + + final BidRequest updatedBidRequest = isExploration + ? bidRequest + : bidRequest.toBuilder() + .imp(updateImps(bidRequest, impsBiddersFilterMap)) + .build(); + final InvocationAction invocationAction = isExploration + ? InvocationAction.no_action + : InvocationAction.update; + final Map> impsBiddersFilterMapToAnalyticsTag = isExploration + ? keepAllBiddersForAnalyticsResult(impsBiddersFilterMap) + : impsBiddersFilterMap; + final Map ort2ImpExtResultMap = createOrtb2ImpExtForImps( + bidRequest, impsBiddersFilterMapToAnalyticsTag, greenbidsId, isExploration); + final AnalyticsResult analyticsResult = AnalyticsResult.of( + "success", ort2ImpExtResultMap, null, null); + + return GreenbidsInvocationResult.of(updatedBidRequest, invocationAction, analyticsResult); + } + + private Boolean isExploration(Partner partner, String greenbidsId) { + final int hashInt = Integer.parseInt( + greenbidsId.substring(greenbidsId.length() - 4), 16); + return hashInt < partner.getExplorationRate() * RANGE_16_BIT_INTEGER_DIVISION_BASIS; + } + + private List updateImps(BidRequest bidRequest, Map> impsBiddersFilterMap) { + return bidRequest.getImp().stream() + .map(imp -> updateImp(imp, impsBiddersFilterMap.get(imp.getId()))) + .toList(); + } + + private Imp updateImp(Imp imp, Map bidderFilterMap) { + return imp.toBuilder() + .ext(updateImpExt(imp.getExt(), bidderFilterMap)) + .build(); + } + + private ObjectNode updateImpExt(ObjectNode impExt, Map bidderFilterMap) { + final ObjectNode updatedExt = impExt.deepCopy(); + Optional.ofNullable((ObjectNode) updatedExt.get("prebid")) + .map(prebidNode -> (ObjectNode) prebidNode.get("bidder")) + .ifPresent(bidderNode -> + bidderFilterMap.entrySet().stream() + .filter(entry -> !entry.getValue()) + .map(Map.Entry::getKey) + .forEach(bidderNode::remove)); + return updatedExt; + } + + private Map> keepAllBiddersForAnalyticsResult( + Map> impsBiddersFilterMap) { + + return impsBiddersFilterMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> true)))); + } + + private Map createOrtb2ImpExtForImps( + BidRequest bidRequest, + Map> impsBiddersFilterMap, + String greenbidsId, + Boolean isExploration) { + + return bidRequest.getImp().stream() + .collect(Collectors.toMap( + Imp::getId, + imp -> createOrtb2ImpExt(imp, impsBiddersFilterMap, greenbidsId, isExploration))); + } + + private Ortb2ImpExtResult createOrtb2ImpExt( + Imp imp, + Map> impsBiddersFilterMap, + String greenbidsId, + Boolean isExploration) { + + final String tid = Optional.ofNullable(imp) + .map(Imp::getExt) + .map(impExt -> impExt.get("tid")) + .map(JsonNode::asText) + .orElse(StringUtils.EMPTY); + final Map impBiddersFilterMap = impsBiddersFilterMap.get(imp.getId()); + final ExplorationResult explorationResult = ExplorationResult.of( + greenbidsId, impBiddersFilterMap, isExploration); + return Ortb2ImpExtResult.of(explorationResult, tid); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java new file mode 100644 index 00000000000..b7450d71560 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import org.apache.commons.lang3.StringUtils; +import ua_parser.Client; +import ua_parser.Device; +import ua_parser.OS; +import ua_parser.Parser; +import ua_parser.UserAgent; + +import java.util.Optional; +import java.util.Set; + +public class GreenbidsUserAgent { + + public static final Set PC_OS_FAMILIES = Set.of( + "Windows 95", "Windows 98", "Solaris"); + + private static final Parser UA_PARSER = new Parser(); + + private final String userAgentString; + + private final UserAgent userAgent; + + private final Device device; + + private final OS os; + + public GreenbidsUserAgent(String userAgentString) { + this.userAgentString = userAgentString; + final Client client = UA_PARSER.parse(userAgentString); + this.userAgent = client.userAgent; + this.device = client.device; + this.os = client.os; + } + + public String getDevice() { + return Optional.ofNullable(device) + .map(device -> isPC() ? "PC" : device.family) + .orElse(StringUtils.EMPTY); + } + + public String getBrowser() { + return Optional.ofNullable(userAgent) + .filter(userAgent -> !"Other".equals(userAgent.family) && StringUtils.isNoneBlank(userAgent.family)) + .map(ua -> "%s %s".formatted(ua.family, StringUtils.defaultString(userAgent.major)).trim()) + .orElse(StringUtils.EMPTY); + } + + private boolean isPC() { + final String osFamily = osFamily(); + return Optional.ofNullable(userAgentString) + .map(userAgent -> userAgent.contains("Windows NT") + || PC_OS_FAMILIES.contains(osFamily) + || ("Windows".equals(osFamily) && "ME".equals(osMajor())) + || ("Mac OS X".equals(osFamily) && !userAgent.contains("Silk")) + || (userAgent.contains("Linux") && userAgent.contains("X11"))) + .orElse(false); + } + + private String osFamily() { + return Optional.ofNullable(os).map(os -> os.family).orElse(StringUtils.EMPTY); + } + + private String osMajor() { + return Optional.ofNullable(os).map(os -> os.major).orElse(StringUtils.EMPTY); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java new file mode 100644 index 00000000000..01087287d44 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java @@ -0,0 +1,96 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ModelCache { + + private static final Logger logger = LoggerFactory.getLogger(ModelCache.class); + + private final String gcsBucketName; + + private final Cache cache; + + private final Storage storage; + + private final String onnxModelCacheKeyPrefix; + + private final AtomicBoolean isFetching; + + private final Vertx vertx; + + private final OnnxModelRunnerFactory onnxModelRunnerFactory; + + public ModelCache( + Storage storage, + String gcsBucketName, + Cache cache, + String onnxModelCacheKeyPrefix, + Vertx vertx, + OnnxModelRunnerFactory onnxModelRunnerFactory) { + this.gcsBucketName = Objects.requireNonNull(gcsBucketName); + this.cache = Objects.requireNonNull(cache); + this.storage = Objects.requireNonNull(storage); + this.onnxModelCacheKeyPrefix = Objects.requireNonNull(onnxModelCacheKeyPrefix); + this.isFetching = new AtomicBoolean(false); + this.vertx = Objects.requireNonNull(vertx); + this.onnxModelRunnerFactory = Objects.requireNonNull(onnxModelRunnerFactory); + } + + public Future get(String onnxModelPath, String pbuid) { + final String cacheKey = onnxModelCacheKeyPrefix + pbuid; + final OnnxModelRunner cachedOnnxModelRunner = cache.getIfPresent(cacheKey); + + if (cachedOnnxModelRunner != null) { + return Future.succeededFuture(cachedOnnxModelRunner); + } + + if (isFetching.compareAndSet(false, true)) { + try { + return fetchAndCacheModelRunner(onnxModelPath, cacheKey); + } finally { + isFetching.set(false); + } + } + + return Future.failedFuture("ModelRunner fetching in progress. Skip current request"); + } + + private Future fetchAndCacheModelRunner(String onnxModelPath, String cacheKey) { + return vertx.executeBlocking(() -> getBlob(onnxModelPath)) + .map(this::loadModelRunner) + .onSuccess(onnxModelRunner -> cache.put(cacheKey, onnxModelRunner)) + .onFailure(error -> logger.error("Failed to fetch ONNX model")); + } + + private Blob getBlob(String onnxModelPath) { + try { + return Optional.ofNullable(storage.get(gcsBucketName)) + .map(bucket -> bucket.get(onnxModelPath)) + .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName)); + } catch (StorageException e) { + throw new PreBidException("Error accessing GCS artefact for model: ", e); + } + } + + private OnnxModelRunner loadModelRunner(Blob blob) { + try { + final byte[] onnxModelBytes = blob.getContent(); + return onnxModelRunnerFactory.create(onnxModelBytes); + } catch (OrtException e) { + throw new PreBidException("Failed to convert blob to ONNX model", e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java new file mode 100644 index 00000000000..d5570f30272 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OrtEnvironment; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; + +import java.util.Collections; + +public class OnnxModelRunner { + + private static final OrtEnvironment ENVIRONMENT = OrtEnvironment.getEnvironment(); + + private final OrtSession session; + + public OnnxModelRunner(byte[] onnxModelBytes) throws OrtException { + session = ENVIRONMENT.createSession(onnxModelBytes, new OrtSession.SessionOptions()); + } + + public OrtSession.Result runModel(String[][] throttlingInferenceRow) throws OrtException { + final OnnxTensor inputTensor = OnnxTensor.createTensor(ENVIRONMENT, throttlingInferenceRow); + return session.run(Collections.singletonMap("input", inputTensor)); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java new file mode 100644 index 00000000000..b6082cf3e12 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; + +public class OnnxModelRunnerFactory { + + public OnnxModelRunner create(byte[] bytes) throws OrtException { + return new OnnxModelRunner(bytes); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java new file mode 100644 index 00000000000..adbc1e17b2c --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import io.vertx.core.Future; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; + +import java.util.Objects; + +public class OnnxModelRunnerWithThresholds { + + private final ModelCache modelCache; + + private final ThresholdCache thresholdCache; + + public OnnxModelRunnerWithThresholds( + ModelCache modelCache, + ThresholdCache thresholdCache) { + this.modelCache = Objects.requireNonNull(modelCache); + this.thresholdCache = Objects.requireNonNull(thresholdCache); + } + + public Future retrieveOnnxModelRunner(Partner partner) { + final String onnxModelPath = "models_pbuid=" + partner.getPbuid() + ".onnx"; + return modelCache.get(onnxModelPath, partner.getPbuid()); + } + + public Future retrieveThreshold(Partner partner) { + final String thresholdJsonPath = "thresholds_pbuid=" + partner.getPbuid() + ".json"; + return thresholdCache.get(thresholdJsonPath, partner.getPbuid()) + .map(partner::getThreshold); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java new file mode 100644 index 00000000000..44eb3d1403a --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java @@ -0,0 +1,102 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ThresholdCache { + + private static final Logger logger = LoggerFactory.getLogger(ThresholdCache.class); + + private final String gcsBucketName; + + private final Cache cache; + + private final Storage storage; + + private final ObjectMapper mapper; + + private final String thresholdsCacheKeyPrefix; + + private final AtomicBoolean isFetching; + + private final Vertx vertx; + + private final ThrottlingThresholdsFactory throttlingThresholdsFactory; + + public ThresholdCache( + Storage storage, + String gcsBucketName, + ObjectMapper mapper, + Cache cache, + String thresholdsCacheKeyPrefix, + Vertx vertx, + ThrottlingThresholdsFactory throttlingThresholdsFactory) { + this.gcsBucketName = Objects.requireNonNull(gcsBucketName); + this.cache = Objects.requireNonNull(cache); + this.storage = Objects.requireNonNull(storage); + this.mapper = Objects.requireNonNull(mapper); + this.thresholdsCacheKeyPrefix = Objects.requireNonNull(thresholdsCacheKeyPrefix); + this.isFetching = new AtomicBoolean(false); + this.vertx = Objects.requireNonNull(vertx); + this.throttlingThresholdsFactory = Objects.requireNonNull(throttlingThresholdsFactory); + } + + public Future get(String thresholdJsonPath, String pbuid) { + final String cacheKey = thresholdsCacheKeyPrefix + pbuid; + final ThrottlingThresholds cachedThrottlingThresholds = cache.getIfPresent(cacheKey); + + if (cachedThrottlingThresholds != null) { + return Future.succeededFuture(cachedThrottlingThresholds); + } + + if (isFetching.compareAndSet(false, true)) { + try { + return fetchAndCacheThrottlingThresholds(thresholdJsonPath, cacheKey); + } finally { + isFetching.set(false); + } + } + + return Future.failedFuture("ThrottlingThresholds fetching in progress. Skip current request"); + } + + private Future fetchAndCacheThrottlingThresholds(String thresholdJsonPath, String cacheKey) { + return vertx.executeBlocking(() -> getBlob(thresholdJsonPath)) + .map(this::loadThrottlingThresholds) + .onSuccess(thresholds -> cache.put(cacheKey, thresholds)) + .onFailure(error -> logger.error("Failed to fetch thresholds")); + } + + private Blob getBlob(String thresholdJsonPath) { + try { + return Optional.ofNullable(storage.get(gcsBucketName)) + .map(bucket -> bucket.get(thresholdJsonPath)) + .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName)); + } catch (StorageException e) { + throw new PreBidException("Error accessing GCS artefact for threshold: ", e); + } + } + + private ThrottlingThresholds loadThrottlingThresholds(Blob blob) { + try { + final byte[] jsonBytes = blob.getContent(); + return throttlingThresholdsFactory.create(jsonBytes, mapper); + } catch (IOException e) { + throw new PreBidException("Failed to load throttling thresholds json", e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java new file mode 100644 index 00000000000..e7ac4a6a4a9 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; + +import java.io.IOException; + +public class ThrottlingThresholdsFactory { + + public ThrottlingThresholds create(byte[] bytes, ObjectMapper mapper) throws IOException { + final JsonNode thresholdsJsonNode = mapper.readTree(bytes); + return mapper.treeToValue(thresholdsJsonNode, ThrottlingThresholds.class); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java new file mode 100644 index 00000000000..2be7c1887e8 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.IntStream; + +@Value(staticConstructor = "of") +public class Partner { + + String pbuid; + + @JsonProperty("targetTpr") + Double targetTpr; + + @JsonProperty("explorationRate") + Double explorationRate; + + public Double getThreshold(ThrottlingThresholds throttlingThresholds) { + final List truePositiveRates = throttlingThresholds.getTpr(); + final List thresholds = throttlingThresholds.getThresholds(); + + final int minSize = Math.min(truePositiveRates.size(), thresholds.size()); + + return IntStream.range(0, minSize) + .filter(i -> truePositiveRates.get(i) >= targetTpr) + .mapToObj(thresholds::get) + .max(Comparator.naturalOrder()) + .orElse(0.0); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java new file mode 100644 index 00000000000..8acb6718936 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class ThrottlingMessage { + + String browser; + + String bidder; + + String adUnitCode; + + String country; + + String hostname; + + String device; + + String hourBucket; + + String minuteQuadrant; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java new file mode 100644 index 00000000000..ccd6594ee38 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ThrottlingThresholds { + + List thresholds; + + List tpr; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java new file mode 100644 index 00000000000..9d175b5b4b3 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result; + +import lombok.Value; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class AnalyticsResult { + + String status; + + Map values; + + String bidder; + + String impId; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java new file mode 100644 index 00000000000..0aff44ceaec --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result; + +import com.iab.openrtb.request.BidRequest; +import lombok.Value; +import org.prebid.server.hooks.v1.InvocationAction; + +@Value(staticConstructor = "of") +public class GreenbidsInvocationResult { + + BidRequest updatedBidRequest; + + InvocationAction invocationAction; + + AnalyticsResult analyticsResult; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..0f20fde7041 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java @@ -0,0 +1,210 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.InvocationResultImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ActivityImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.AppliedToImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ResultImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.TagsImpl; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class GreenbidsRealTimeDataProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "greenbids-real-time-data-processed-auction-request"; + private static final String ACTIVITY = "greenbids-filter"; + private static final String SUCCESS_STATUS = "success"; + private static final String BID_REQUEST_ANALYTICS_EXTENSION_NAME = "greenbids-rtd"; + + private final ObjectMapper mapper; + private final FilterService filterService; + private final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds; + private final GreenbidsInferenceDataService greenbidsInferenceDataService; + private final GreenbidsInvocationService greenbidsInvocationService; + + public GreenbidsRealTimeDataProcessedAuctionRequestHook( + ObjectMapper mapper, + FilterService filterService, + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds, + GreenbidsInferenceDataService greenbidsInferenceDataService, + GreenbidsInvocationService greenbidsInvocationService) { + this.mapper = Objects.requireNonNull(mapper); + this.filterService = Objects.requireNonNull(filterService); + this.onnxModelRunnerWithThresholds = Objects.requireNonNull(onnxModelRunnerWithThresholds); + this.greenbidsInferenceDataService = Objects.requireNonNull(greenbidsInferenceDataService); + this.greenbidsInvocationService = Objects.requireNonNull(greenbidsInvocationService); + } + + @Override + public Future> call( + AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final AuctionContext auctionContext = invocationContext.auctionContext(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Partner partner = parseBidRequestExt(bidRequest); + + if (partner == null) { + return Future.succeededFuture(toInvocationResult( + bidRequest, null, InvocationAction.no_action)); + } + + return Future.all( + onnxModelRunnerWithThresholds.retrieveOnnxModelRunner(partner), + onnxModelRunnerWithThresholds.retrieveThreshold(partner)) + .compose(compositeFuture -> toInvocationResult( + bidRequest, + partner, + compositeFuture.resultAt(0), + compositeFuture.resultAt(1))) + .recover(throwable -> Future.succeededFuture(toInvocationResult( + bidRequest, null, InvocationAction.no_action))); + } + + private Partner parseBidRequestExt(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .filter(this::isNotEmptyObjectNode) + .map(analytics -> (ObjectNode) analytics.get(BID_REQUEST_ANALYTICS_EXTENSION_NAME)) + .map(this::toPartner) + .orElse(null); + } + + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + + private Partner toPartner(ObjectNode adapterNode) { + try { + return mapper.treeToValue(adapterNode, Partner.class); + } catch (JsonProcessingException e) { + return null; + } + } + + private Future> toInvocationResult( + BidRequest bidRequest, + Partner partner, + OnnxModelRunner onnxModelRunner, + Double threshold) { + + final Map> impsBiddersFilterMap; + try { + final List throttlingMessages = greenbidsInferenceDataService + .extractThrottlingMessagesFromBidRequest(bidRequest); + + impsBiddersFilterMap = filterService.filterBidders( + onnxModelRunner, + throttlingMessages, + threshold); + } catch (PreBidException e) { + return Future.succeededFuture(toInvocationResult( + bidRequest, null, InvocationAction.no_action)); + } + + final GreenbidsInvocationResult greenbidsInvocationResult = greenbidsInvocationService + .createGreenbidsInvocationResult(partner, bidRequest, impsBiddersFilterMap); + + return Future.succeededFuture(toInvocationResult( + greenbidsInvocationResult.getUpdatedBidRequest(), + greenbidsInvocationResult.getAnalyticsResult(), + greenbidsInvocationResult.getInvocationAction())); + } + + private InvocationResult toInvocationResult( + BidRequest bidRequest, + AnalyticsResult analyticsResult, + InvocationAction action) { + + final List analyticsResults = analyticsResult != null + ? Collections.singletonList(analyticsResult) + : Collections.emptyList(); + + return switch (action) { + case InvocationAction.update -> InvocationResultImpl + .builder() + .status(InvocationStatus.success) + .action(action) + .payloadUpdate(payload -> AuctionRequestPayloadImpl.of(bidRequest)) + .analyticsTags(toAnalyticsTags(analyticsResults)) + .build(); + default -> InvocationResultImpl + .builder() + .status(InvocationStatus.success) + .action(action) + .analyticsTags(toAnalyticsTags(analyticsResults)) + .build(); + }; + } + + private Tags toAnalyticsTags(List analyticsResults) { + if (CollectionUtils.isEmpty(analyticsResults)) { + return null; + } + + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY, + SUCCESS_STATUS, + toResults(analyticsResults)))); + } + + private List toResults(List analyticsResults) { + return analyticsResults.stream() + .map(this::toResult) + .toList(); + } + + private Result toResult(AnalyticsResult analyticsResult) { + return ResultImpl.of( + analyticsResult.getStatus(), + toObjectNode(analyticsResult.getValues()), + AppliedToImpl.builder() + .bidders(Collections.singletonList(analyticsResult.getBidder())) + .impIds(Collections.singletonList(analyticsResult.getImpId())) + .build()); + } + + private ObjectNode toObjectNode(Map values) { + return values != null ? mapper.valueToTree(values) : null; + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..4efcb2bd5c2 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Object moduleContext; + + Tags analyticsTags; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java new file mode 100644 index 00000000000..421541e59da --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ActivityImpl implements Activity { + + String name; + + String status; + + List results; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java new file mode 100644 index 00000000000..68ad76ccdf3 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.AppliedTo; + +import java.util.List; + +@Accessors(fluent = true) +@Value +@Builder +public class AppliedToImpl implements AppliedTo { + + List impIds; + + List bidders; + + boolean request; + + boolean response; + + List bidIds; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java new file mode 100644 index 00000000000..d234a59eb31 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ResultImpl implements Result { + + String status; + + ObjectNode values; + + AppliedTo appliedTo; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java new file mode 100644 index 00000000000..899e797dab2 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class TagsImpl implements Tags { + + List activities; +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java new file mode 100644 index 00000000000..0a3ab82b9cd --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java @@ -0,0 +1,177 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OnnxValue; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import ai.onnxruntime.TensorInfo; +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.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FilterServiceTest { + + @Mock + private OnnxModelRunner onnxModelRunnerMock; + + @Mock + private OrtSession.Result results; + + @Mock + private OnnxTensor onnxTensor; + + @Mock + private TensorInfo tensorInfo; + + @Mock + private OnnxValue onnxValue; + + private final FilterService target = new FilterService(); + + @Test + public void filterBiddersShouldReturnFilteredBiddersWhenValidThrottlingMessagesProvided() + throws OrtException, IOException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + final OnnxModelRunner onnxModelRunner = givenOnnxModelRunner(); + + // when + final Map> impsBiddersFilterMap = target.filterBidders( + onnxModelRunner, throttlingMessages, threshold); + + // then + assertThat(impsBiddersFilterMap).isNotNull(); + assertThat(impsBiddersFilterMap.get("adUnit1").get("bidder1")).isTrue(); + assertThat(impsBiddersFilterMap.get("adUnit2").get("bidder2")).isFalse(); + assertThat(impsBiddersFilterMap.get("adUnit3").get("bidder3")).isFalse(); + } + + @Test + public void validateOnnxTensorShouldThrowPreBidExceptionWhenOnnxValueIsNotTensor() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results); + when(results.spliterator()).thenReturn(Arrays.asList(createInvalidOnnxItem()).spliterator()); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Expected OnnxTensor for 'probabilities', but found"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenOrtExceptionOccurs() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))) + .thenThrow(new OrtException("Exception during runModel")); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Exception during model inference"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenThrottlingMessagesIsEmpty() { + // given + final List throttlingMessages = Collections.emptyList(); + final Double threshold = 0.5; + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("throttlingMessages cannot be null or empty"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenTensorSizeMismatchOccurs() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results); + when(results.spliterator()).thenReturn(Arrays.asList(createOnnxItem()).spliterator()); + when(onnxTensor.getInfo()).thenReturn(tensorInfo); + when(tensorInfo.getShape()).thenReturn(new long[]{0}); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Mismatch between tensor size and throttlingMessages size"); + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } + + private List createThrottlingMessages() { + final ThrottlingMessage throttlingMessage1 = ThrottlingMessage.builder() + .browser("Chrome") + .bidder("bidder1") + .adUnitCode("adUnit1") + .country("US") + .hostname("localhost") + .device("PC") + .hourBucket("10") + .minuteQuadrant("1") + .build(); + + final ThrottlingMessage throttlingMessage2 = ThrottlingMessage.builder() + .browser("Firefox") + .bidder("bidder2") + .adUnitCode("adUnit2") + .country("FR") + .hostname("www.leparisien.fr") + .device("Mobile") + .hourBucket("11") + .minuteQuadrant("2") + .build(); + + final ThrottlingMessage throttlingMessage3 = ThrottlingMessage.builder() + .browser("Safari") + .bidder("bidder3") + .adUnitCode("adUnit3") + .country("FR") + .hostname("www.lesechos.fr") + .device("Tablet") + .hourBucket("12") + .minuteQuadrant("3") + .build(); + + return Arrays.asList(throttlingMessage1, throttlingMessage2, throttlingMessage3); + } + + private Map.Entry createOnnxItem() { + return new AbstractMap.SimpleEntry<>("probabilities", onnxTensor); + } + + private Map.Entry createInvalidOnnxItem() { + return new AbstractMap.SimpleEntry<>("probabilities", onnxValue); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java new file mode 100644 index 00000000000..1ac1bcc5cb1 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java @@ -0,0 +1,165 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.Country; +import org.apache.commons.lang3.StringUtils; +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.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; + +import java.io.IOException; +import java.net.InetAddress; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsInferenceDataServiceTest { + + @Mock(strictness = LENIENT) + private DatabaseReaderFactory databaseReaderFactory; + + @Mock + private DatabaseReader databaseReader; + + @Mock + private Country country; + + private GreenbidsInferenceDataService target; + + @BeforeEach + public void setUp() { + when(databaseReaderFactory.getDatabaseReader()).thenReturn(databaseReader); + target = new GreenbidsInferenceDataService(databaseReaderFactory, TestBidRequestProvider.MAPPER); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldReturnValidThrottlingMessages() + throws IOException, GeoIp2Exception { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + + final CountryResponse countryResponse = mock(CountryResponse.class); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + when(databaseReader.country(any(InetAddress.class))).thenReturn(countryResponse); + when(countryResponse.getCountry()).thenReturn(country); + when(country.getName()).thenReturn("US"); + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + assertThat(throttlingMessages.getFirst().getBidder()).isEqualTo("rubicon"); + assertThat(throttlingMessages.get(1).getBidder()).isEqualTo("appnexus"); + assertThat(throttlingMessages.getLast().getBidder()).isEqualTo("pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo("US"); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldHandleMissingIp() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDeviceWithoutIp(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + + assertThat(throttlingMessages.getFirst().getBidder()).isEqualTo("rubicon"); + assertThat(throttlingMessages.get(1).getBidder()).isEqualTo("appnexus"); + assertThat(throttlingMessages.getLast().getBidder()).isEqualTo("pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo(StringUtils.EMPTY); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldThrowPreBidExceptionWhenGeoIpFails() + throws IOException, GeoIp2Exception { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + + when(databaseReader.country(any(InetAddress.class))).thenThrow(new GeoIp2Exception("GeoIP failure")); + + // when & then + assertThatThrownBy(() -> target.extractThrottlingMessagesFromBidRequest(bidRequest)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Failed to fetch country from geoLite DB"); + } + + private Device givenDeviceWithoutIp(UnaryOperator deviceCustomizer) { + final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36"; + return deviceCustomizer.apply(Device.builder().ua(userAgent)).build(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java new file mode 100644 index 00000000000..1bf0f5409e4 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java @@ -0,0 +1,126 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsInvocationServiceTest { + + private GreenbidsInvocationService target; + + @BeforeEach + public void setUp() { + target = new GreenbidsInvocationService(); + } + + @Test + public void createGreenbidsInvocationResultShouldReturnUpdateBidRequestWhenNotExploration() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + final Map> impsBiddersFilterMap = givenImpsBiddersFilterMap(); + final Partner partner = givenPartner(0.0); + + // when + final GreenbidsInvocationResult result = target.createGreenbidsInvocationResult( + partner, bidRequest, impsBiddersFilterMap); + + // then + final JsonNode updatedBidRequestExtPrebidBidders = result.getUpdatedBidRequest().getImp().getFirst().getExt() + .get("prebid").get("bidder"); + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.update); + assertThat(updatedBidRequestExtPrebidBidders.has("rubicon")).isTrue(); + assertThat(updatedBidRequestExtPrebidBidders.has("appnexus")).isFalse(); + assertThat(updatedBidRequestExtPrebidBidders.has("pubmatic")).isFalse(); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isFalse(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isTrue(); + assertThat(keptInAuction.get("appnexus")).isFalse(); + assertThat(keptInAuction.get("pubmatic")).isFalse(); + + } + + @Test + public void createGreenbidsInvocationResultShouldReturnNoActionWhenExploration() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + final Map> impsBiddersFilterMap = givenImpsBiddersFilterMap(); + final Partner partner = givenPartner(1.0); + + // when + final GreenbidsInvocationResult result = target.createGreenbidsInvocationResult( + partner, bidRequest, impsBiddersFilterMap); + + // then + final JsonNode updatedBidRequestExtPrebidBidders = result.getUpdatedBidRequest().getImp().getFirst().getExt() + .get("prebid").get("bidder"); + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.no_action); + assertThat(updatedBidRequestExtPrebidBidders.has("rubicon")).isTrue(); + assertThat(updatedBidRequestExtPrebidBidders.has("appnexus")).isTrue(); + assertThat(updatedBidRequestExtPrebidBidders.has("pubmatic")).isTrue(); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isTrue(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isTrue(); + assertThat(keptInAuction.get("appnexus")).isTrue(); + assertThat(keptInAuction.get("pubmatic")).isTrue(); + } + + private Map> givenImpsBiddersFilterMap() { + final Map biddersFitlerMap = new HashMap<>(); + biddersFitlerMap.put("rubicon", true); + biddersFitlerMap.put("appnexus", false); + biddersFitlerMap.put("pubmatic", false); + + final Map> impsBiddersFilterMap = new HashMap<>(); + impsBiddersFilterMap.put("adunitcodevalue", biddersFitlerMap); + + return impsBiddersFilterMap; + } + + private Partner givenPartner(Double explorationRate) { + return Partner.of("test-pbuid", 0.60, explorationRate); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java new file mode 100644 index 00000000000..b4839146a44 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java @@ -0,0 +1,59 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GreenbidsUserAgentTest { + + @Test + public void getDeviceShouldReturnPCWhenWindowsNTInUserAgent() { + // given + final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getDevice()).isEqualTo("PC"); + } + + @Test + public void getDeviceShouldReturnDeviceIPhoneWhenIOSInUserAgent() { + // given + final String userAgentString = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getDevice()).isEqualTo("iPhone"); + } + + @Test + public void getBrowserShouldReturnBrowserNameAndVersionWhenUserAgentIsPresent() { + // given + final String userAgentString = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getBrowser()).isEqualTo("Chrome 58"); + } + + @Test + public void getBrowserShouldReturnEmptyStringWhenBrowserIsNull() { + // given + final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getBrowser()).isEqualTo(StringUtils.EMPTY); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java new file mode 100644 index 00000000000..0c326f4249e --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java @@ -0,0 +1,191 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import com.google.cloud.storage.Blob; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.exception.PreBidException; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ModelCacheTest { + + private static final String GCS_BUCKET_NAME = "test_bucket"; + private static final String MODEL_CACHE_KEY_PREFIX = "onnxModelRunner_"; + private static final String PBUUID = "test-pbuid"; + private static final String ONNX_MODEL_PATH = "model.onnx"; + + @Mock + private Cache cache; + + @Mock(strictness = LENIENT) + private Storage storage; + + @Mock(strictness = LENIENT) + private Bucket bucket; + + @Mock(strictness = LENIENT) + private Blob blob; + + @Mock + private OnnxModelRunner onnxModelRunner; + + @Mock(strictness = LENIENT) + private OnnxModelRunnerFactory onnxModelRunnerFactory; + + @Mock + private ModelCache target; + + private Vertx vertx; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new ModelCache( + storage, GCS_BUCKET_NAME, cache, MODEL_CACHE_KEY_PREFIX, vertx, onnxModelRunnerFactory); + } + + @Test + public void getShouldReturnModelFromCacheWhenPresent() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(onnxModelRunner); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(onnxModelRunner); + verify(cache).getIfPresent(eq(cacheKey)); + } + + @Test + public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + final ModelCache spyModelCache = spy(target); + final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(mockFetchingState.compareAndSet(false, true)).thenReturn(false); + final Field isFetchingField = ModelCache.class.getDeclaredField("isFetching"); + isFetchingField.setAccessible(true); + isFetchingField.set(spyModelCache, mockFetchingState); + + // when + final Future result = spyModelCache.get(ONNX_MODEL_PATH, PBUUID); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo( + "ModelRunner fetching in progress. Skip current request"); + } + + @Test + public void getShouldFetchModelWhenNotInCache() throws OrtException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + final byte[] bytes = new byte[]{1, 2, 3}; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(onnxModelRunnerFactory.create(bytes)).thenReturn(onnxModelRunner); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result()).isEqualTo(onnxModelRunner); + verify(cache).put(eq(cacheKey), eq(onnxModelRunner)); + }); + } + + @Test + public void getShouldThrowExceptionWhenStorageFails() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for model"); + }); + } + + @Test + public void getShouldThrowExceptionWhenOnnxModelFails() throws OrtException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + final byte[] bytes = new byte[]{1, 2, 3}; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(onnxModelRunnerFactory.create(bytes)).thenThrow( + new OrtException("Failed to convert blob to ONNX model")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Failed to convert blob to ONNX model"); + }); + } + + @Test + public void getShouldThrowExceptionWhenBucketNotFound() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenThrow(new PreBidException("Bucket not found")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Bucket not found"); + }); + } + +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java new file mode 100644 index 00000000000..4a18b0a0fc0 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java @@ -0,0 +1,73 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Objects; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class OnnxModelRunnerTest { + + private OnnxModelRunner target; + + @BeforeEach + public void setUp() throws OrtException, IOException { + target = givenOnnxModelRunner(); + } + + @Test + public void runModelShouldReturnProbabilitiesWhenValidThrottlingInferenceRow() throws OrtException { + // given + final String[][] throttlingInferenceRow = {{ + "Chrome 59", "rubicon", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}}; + + // when + final OrtSession.Result actualResult = target.runModel(throttlingInferenceRow); + + // then + final float[][] probabilities = StreamSupport.stream(actualResult.spliterator(), false) + .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities")) + .map(Map.Entry::getValue) + .map(OnnxTensor.class::cast) + .map(tensor -> { + try { + return (float[][]) tensor.getValue(); + } catch (OrtException e) { + throw new RuntimeException(e); + } + }).findFirst().get(); + + assertThat(actualResult).isNotNull(); + assertThat(actualResult).hasSize(2); + assertThat(probabilities[0]).isNotEmpty(); + assertThat(probabilities[0][0]).isBetween(0.0f, 1.0f); + assertThat(probabilities[0][1]).isBetween(0.0f, 1.0f); + } + + @Test + public void runModelShouldThrowOrtExceptionWhenNonValidThrottlingInferenceRow() { + // given + final String[][] throttlingInferenceRowWithMissingColumn = {{ + "Chrome 59", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}}; + + // when & then + assertThatThrownBy(() -> target.runModel(throttlingInferenceRowWithMissingColumn)) + .isInstanceOf(OrtException.class); + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java new file mode 100644 index 00000000000..90a8d521f71 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java @@ -0,0 +1,198 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +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.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ThresholdCacheTest { + + private static final String GCS_BUCKET_NAME = "test_bucket"; + private static final String THRESHOLD_CACHE_KEY_PREFIX = "onnxModelRunner_"; + private static final String PBUUID = "test-pbuid"; + private static final String THRESHOLDS_PATH = "thresholds.json"; + + @Mock + private Cache cache; + + @Mock(strictness = LENIENT) + private Storage storage; + + @Mock(strictness = LENIENT) + private Bucket bucket; + + @Mock(strictness = LENIENT) + private Blob blob; + + @Mock + private ThrottlingThresholds throttlingThresholds; + + @Mock(strictness = LENIENT) + private ThrottlingThresholdsFactory throttlingThresholdsFactory; + + private Vertx vertx; + + private ThresholdCache target; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new ThresholdCache( + storage, + GCS_BUCKET_NAME, + TestBidRequestProvider.MAPPER, + cache, + THRESHOLD_CACHE_KEY_PREFIX, + vertx, + throttlingThresholdsFactory); + } + + @Test + public void getShouldReturnThresholdsFromCacheWhenPresent() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(throttlingThresholds); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(throttlingThresholds); + verify(cache).getIfPresent(eq(cacheKey)); + } + + @Test + public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + + final ThresholdCache spyThresholdCache = spy(target); + final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(mockFetchingState.compareAndSet(false, true)).thenReturn(false); + + final Field isFetchingField = ThresholdCache.class.getDeclaredField("isFetching"); + isFetchingField.setAccessible(true); + isFetchingField.set(spyThresholdCache, mockFetchingState); + + // when + final Future result = spyThresholdCache.get(THRESHOLDS_PATH, PBUUID); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo( + "ThrottlingThresholds fetching in progress. Skip current request"); + } + + @Test + public void getShouldFetchThresholdsWhenNotInCache() throws IOException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + final String jsonContent = "test_json_content"; + final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)) + .thenReturn(throttlingThresholds); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result()).isEqualTo(throttlingThresholds); + verify(cache).put(eq(cacheKey), eq(throttlingThresholds)); + }); + } + + @Test + public void getShouldThrowExceptionWhenStorageFails() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for threshold"); + }); + } + + @Test + public void getShouldThrowExceptionWhenLoadingJsonFails() throws IOException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + final String jsonContent = "test_json_content"; + final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8); + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)).thenThrow( + new IOException("Failed to load throttling thresholds json")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Failed to load throttling thresholds json"); + }); + } + + @Test + public void getShouldThrowExceptionWhenBucketNotFound() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenThrow(new PreBidException("Bucket not found")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Bucket not found"); + }); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java new file mode 100644 index 00000000000..11ca069e447 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java @@ -0,0 +1,93 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +public class TestBidRequestProvider { + + public static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + private TestBidRequestProvider() { } + + public static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + List imps, + Device device, + ExtRequest extRequest) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("request") + .imp(imps) + .site(givenSite(site -> site)) + .device(device) + .ext(extRequest)).build(); + } + + public static Site givenSite(UnaryOperator siteCustomizer) { + return siteCustomizer.apply(Site.builder().domain("www.leparisien.fr")).build(); + } + + public static ObjectNode givenImpExt() { + final ObjectNode bidderNode = MAPPER.createObjectNode(); + + final ObjectNode rubiconNode = MAPPER.createObjectNode(); + rubiconNode.put("accountId", 1001); + rubiconNode.put("siteId", 267318); + rubiconNode.put("zoneId", 1861698); + bidderNode.set("rubicon", rubiconNode); + + final ObjectNode appnexusNode = MAPPER.createObjectNode(); + appnexusNode.put("placementId", 123456); + bidderNode.set("appnexus", appnexusNode); + + final ObjectNode pubmaticNode = MAPPER.createObjectNode(); + pubmaticNode.put("publisherId", "156209"); + pubmaticNode.put("adSlot", "slot1@300x250"); + bidderNode.set("pubmatic", pubmaticNode); + + final ObjectNode prebidNode = MAPPER.createObjectNode(); + prebidNode.set("bidder", bidderNode); + + final ObjectNode extNode = MAPPER.createObjectNode(); + extNode.set("prebid", prebidNode); + extNode.set("tid", TextNode.valueOf("67eaab5f-27a6-4689-93f7-bd8f024576e3")); + + return extNode; + } + + public static Device givenDevice(UnaryOperator deviceCustomizer) { + final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36"; + return deviceCustomizer.apply(Device.builder().ua(userAgent).ip("151.101.194.216")).build(); + } + + public static Device givenDeviceWithoutUserAgent(UnaryOperator deviceCustomizer) { + return deviceCustomizer.apply(Device.builder().ip("151.101.194.216")).build(); + } + + public static Banner givenBanner() { + final Format format = Format.builder() + .w(320) + .h(50) + .build(); + + return Banner.builder() + .format(Collections.singletonList(format)) + .w(240) + .h(400) + .build(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..fed56cf009e --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java @@ -0,0 +1,473 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1; + +import ai.onnxruntime.OrtException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +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.analytics.reporter.greenbids.model.ExplorationResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ActivityImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.AppliedToImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ResultImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.TagsImpl; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.model.HttpRequestContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDeviceWithoutUserAgent; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenSite; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsRealTimeDataProcessedAuctionRequestHookTest { + + @Mock + private Cache modelCacheWithExpiration; + + @Mock + private Cache thresholdsCacheWithExpiration; + + @Mock(strictness = LENIENT) + private DatabaseReaderFactory databaseReaderFactory; + + private GreenbidsRealTimeDataProcessedAuctionRequestHook target; + + @BeforeEach + public void setUp() throws IOException { + final Storage storage = StorageOptions.newBuilder() + .setProjectId("test_project").build().getService(); + final DatabaseReader dbReader = givenDatabaseReader(); + final FilterService filterService = new FilterService(); + final OnnxModelRunnerFactory onnxModelRunnerFactory = new OnnxModelRunnerFactory(); + final ThrottlingThresholdsFactory throttlingThresholdsFactory = new ThrottlingThresholdsFactory(); + final ModelCache modelCache = new ModelCache( + storage, + "test_bucket", + modelCacheWithExpiration, + "onnxModelRunner_", + Vertx.vertx(), + onnxModelRunnerFactory); + final ThresholdCache thresholdCache = new ThresholdCache( + storage, + "test_bucket", + TestBidRequestProvider.MAPPER, + thresholdsCacheWithExpiration, + "throttlingThresholds_", + Vertx.vertx(), + throttlingThresholdsFactory); + final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds = new OnnxModelRunnerWithThresholds( + modelCache, + thresholdCache); + when(databaseReaderFactory.getDatabaseReader()).thenReturn(dbReader); + final GreenbidsInferenceDataService greenbidsInferenceDataService = new GreenbidsInferenceDataService( + databaseReaderFactory, + TestBidRequestProvider.MAPPER); + final GreenbidsInvocationService greenbidsInvocationService = new GreenbidsInvocationService(); + target = new GreenbidsRealTimeDataProcessedAuctionRequestHook( + TestBidRequestProvider.MAPPER, + filterService, + onnxModelRunnerWithThresholds, + greenbidsInferenceDataService, + greenbidsInvocationService); + } + + @Test + public void callShouldExitEarlyWhenPartnerNotActivatedInBidRequest() { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Device device = givenDevice(identity()); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.analyticsTags()).isNull(); + } + + @Test + public void callShouldNotFilterBiddersAndReturnAnalyticsTagWhenExploration() throws OrtException, IOException { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Double explorationRate = 1.0; + final Device device = givenDevice(identity()); + final ExtRequest extRequest = givenExtRequest(explorationRate); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid")) + .thenReturn(givenOnnxModelRunner()); + when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid")) + .thenReturn(givenThrottlingThresholds()); + + final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(true, true); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + + // then + final ActivityImpl activity = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl resultImpl = (ResultImpl) activity.results().getFirst(); + final String fingerprint = resultImpl.values() + .get("adunitcodevalue") + .get("greenbids") + .get("fingerprint").asText(); + + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags()).usingRecursiveComparison() + .ignoringFields( + "activities.results" + + ".values._children" + + ".adunitcodevalue._children" + + ".greenbids._children.fingerprint") + .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult))); + assertThat(fingerprint).isNotNull(); + } + + @Test + public void callShouldFilterBiddersBasedOnModelWhenAnyFeatureNotAvailable() throws OrtException, IOException { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Double explorationRate = 0.0001; + final Device device = givenDeviceWithoutUserAgent(identity()); + final ExtRequest extRequest = givenExtRequest(explorationRate); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid")) + .thenReturn(givenOnnxModelRunner()); + when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid")) + .thenReturn(givenThrottlingThresholds()); + + final BidRequest expectedBidRequest = expectedUpdatedBidRequest(request -> request, explorationRate, device); + final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(false, false); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + final BidRequest resultBidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(bidRequest)) + .bidRequest(); + + // then + final ActivityImpl activity = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl resultImpl = (ResultImpl) activity.results().getFirst(); + final String fingerprint = resultImpl.values() + .get("adunitcodevalue") + .get("greenbids") + .get("fingerprint").asText(); + + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags()).usingRecursiveComparison() + .ignoringFields( + "activities.results" + + ".values._children" + + ".adunitcodevalue._children" + + ".greenbids._children.fingerprint") + .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult))); + assertThat(fingerprint).isNotNull(); + assertThat(resultBidRequest).usingRecursiveComparison().isEqualTo(expectedBidRequest); + } + + @Test + public void callShouldFilterBiddersBasedOnModelResults() throws OrtException, IOException { + // given + final Banner banner = givenBanner(); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final Double explorationRate = 0.0001; + final Device device = givenDevice(identity()); + final ExtRequest extRequest = givenExtRequest(explorationRate); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid")) + .thenReturn(givenOnnxModelRunner()); + when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid")) + .thenReturn(givenThrottlingThresholds()); + + final BidRequest expectedBidRequest = expectedUpdatedBidRequest( + request -> request, explorationRate, device); + final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(false, false); + + // when + final Future> future = target + .call(null, invocationContext); + final InvocationResult result = future.result(); + final BidRequest resultBidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(bidRequest)) + .bidRequest(); + + // then + final ActivityImpl activityImpl = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl resultImpl = (ResultImpl) activityImpl.results().getFirst(); + final String fingerprint = resultImpl.values() + .get("adunitcodevalue") + .get("greenbids") + .get("fingerprint").asText(); + + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags()).usingRecursiveComparison() + .ignoringFields( + "activities.results" + + ".values._children" + + ".adunitcodevalue._children" + + ".greenbids._children.fingerprint") + .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult))); + assertThat(fingerprint).isNotNull(); + assertThat(resultBidRequest).usingRecursiveComparison() + .isEqualTo(expectedBidRequest); + } + + static DatabaseReader givenDatabaseReader() throws IOException { + final URL url = new URL("https://git.io/GeoLite2-Country.mmdb"); + final Path databasePath = Files.createTempFile("GeoLite2-Country", ".mmdb"); + + try ( + InputStream inputStream = url.openStream(); + FileOutputStream outputStream = new FileOutputStream(databasePath.toFile())) { + inputStream.transferTo(outputStream); + } + + return new DatabaseReader.Builder(databasePath.toFile()).build(); + } + + static ExtRequest givenExtRequest(Double explorationRate) { + final ObjectNode greenbidsNode = TestBidRequestProvider.MAPPER.createObjectNode(); + greenbidsNode.put("pbuid", "test-pbuid"); + greenbidsNode.put("targetTpr", 0.60); + greenbidsNode.put("explorationRate", explorationRate); + + final ObjectNode analyticsNode = TestBidRequestProvider.MAPPER.createObjectNode(); + analyticsNode.set("greenbids-rtd", greenbidsNode); + + return ExtRequest.of(ExtRequestPrebid + .builder() + .analytics(analyticsNode) + .build()); + } + + private AuctionContext givenAuctionContext( + BidRequest bidRequest, + UnaryOperator auctionContextCustomizer) { + + final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .httpRequest(HttpRequestContext.builder().build()) + .bidRequest(bidRequest); + + return auctionContextCustomizer.apply(auctionContextBuilder).build(); + } + + private AuctionInvocationContext givenAuctionInvocationContext(AuctionContext auctionContext) { + final AuctionInvocationContext invocationContext = mock(AuctionInvocationContext.class); + when(invocationContext.auctionContext()).thenReturn(auctionContext); + return invocationContext; + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } + + private ThrottlingThresholds givenThrottlingThresholds() throws IOException { + final JsonNode thresholdsJsonNode = TestBidRequestProvider.MAPPER.readTree( + Files.newInputStream(Paths.get( + "src/test/resources/thresholds_pbuid=test-pbuid.json"))); + return TestBidRequestProvider.MAPPER + .treeToValue(thresholdsJsonNode, ThrottlingThresholds.class); + } + + private BidRequest expectedUpdatedBidRequest( + UnaryOperator bidRequestCustomizer, + Double explorationRate, + Device device) { + + final Banner banner = givenBanner(); + + final ObjectNode bidderNode = TestBidRequestProvider.MAPPER.createObjectNode(); + final ObjectNode prebidNode = TestBidRequestProvider.MAPPER.createObjectNode(); + prebidNode.set("bidder", bidderNode); + + final ObjectNode extNode = TestBidRequestProvider.MAPPER.createObjectNode(); + extNode.set("prebid", prebidNode); + extNode.set("tid", TextNode.valueOf("67eaab5f-27a6-4689-93f7-bd8f024576e3")); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(extNode) + .banner(banner) + .build(); + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("request") + .imp(List.of(imp)) + .site(givenSite(site -> site)) + .device(device) + .ext(givenExtRequest(explorationRate))).build(); + } + + private AnalyticsResult expectedAnalyticsResult(Boolean isExploration, Boolean isKeptInAuction) { + return AnalyticsResult.of( + "success", + Map.of("adunitcodevalue", expectedOrtb2ImpExtResult(isExploration, isKeptInAuction)), + null, + null); + } + + private Ortb2ImpExtResult expectedOrtb2ImpExtResult(Boolean isExploration, Boolean isKeptInAuction) { + return Ortb2ImpExtResult.of( + expectedExplorationResult(isExploration, isKeptInAuction), "67eaab5f-27a6-4689-93f7-bd8f024576e3"); + } + + private ExplorationResult expectedExplorationResult(Boolean isExploration, Boolean isKeptInAuction) { + final Map keptInAuction = Map.of( + "appnexus", isKeptInAuction, + "pubmatic", isKeptInAuction, + "rubicon", isKeptInAuction); + return ExplorationResult.of("60a7c66c-c542-48c6-a319-ea7b9f97947f", keptInAuction, isExploration); + } + + private Tags toAnalyticsTags(List analyticsResults) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + "greenbids-filter", + "success", + toResults(analyticsResults)))); + } + + private List toResults(List analyticsResults) { + return analyticsResults.stream() + .map(this::toResult) + .toList(); + } + + private Result toResult(AnalyticsResult analyticsResult) { + return ResultImpl.of( + analyticsResult.getStatus(), + toObjectNode(analyticsResult.getValues()), + AppliedToImpl.builder() + .bidders(Collections.singletonList(analyticsResult.getBidder())) + .impIds(Collections.singletonList(analyticsResult.getImpId())) + .build()); + } + + private ObjectNode toObjectNode(Map values) { + return values != null ? TestBidRequestProvider.MAPPER.valueToTree(values) : null; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx new file mode 100644 index 0000000000000000000000000000000000000000..f0acc8c66fe2e0902e00ea06d9b8373aa80e735a GIT binary patch literal 4212 zcmchaZ*Su?9LJNaOEa(C?ZWMJ6rJu=34|gvE&pNCpnvFu%5=aTNC+N8Y3pv$(qxL$ z-R=<}9s*Bz1$z@nh)2ADb}^ao4abf=0FTbL@}5{>4ArQMObCorgvoMOO`3 z-`Ab}gJ#ta_DR9pf*~CK_85E#HiPJ5nv!e;uhg9}^wVhqy=CxW6{Q(R;7!BhQOd7QlV)m%j zELfTbYUl6;@=%%t(JAW7-LoW~z<%{ONZ>fW>dVjkv7ZFjqM|(j^-DjTLgWQe>R6~H zw8WNlEmjMm@v{i{U{H4og7ijxQIXC`dWVx#9Rwph486~8jQqyDy{I3TL>ZhYqbSPY zM7_m{+9a;k`&nkt>=opEza+=xo9UJ4@= zC+bd|jDpAyy?C19Ewxd73bW_U{N_*q9|A7WImVqLLvJVyqL)#Oa*8pd7-bZ*oMO%> zW*NmQr&u$JRYtMPDfWzFmr-ZCLU4jf-OxuU7zZ$Et|-^` z!Pbz}@`4fBMN2QdK4?UCRzJeK4rzn;-Rz(3z;qvGDS09k+N4-p-jA5I+GuUGDy>a? z$*n5B6nt%wHiNvM=P)=uOVQCEFJB%V3HV2oxjDbP#nV~kYf;wT0BSat%#n*n2_m2i zR5C>5jr|b8e6)B(_@e2moINFwHrQnlda@^juVjdgESKI>ObdhKJDt}3T>{)?xjOtOEy59(AoN$G6PW;%<4JP!DL77erZi_t)8&m$s_ zi99N3T(HRKfV;GR;`dyGn7sg+5*BrPF=rNW|2LpU8T1sLPo_M$1mSlw!R98tyP@nB z^_HrT8TC5Yxxi;KGKC^Ej+05V^5l~zy=!q}@dU3vKCs}}vaFHqj2#QQw*T(IdycC+ z=D^W?V>s4oyVXNc=+MtSBj|D^pWqra{dOW}4KjFw>&u zCNphn%FJ}AS!JgC)gJpSS8D($iYfyENQyGJ3)r%)09mQ35)cLtL2(JsT8ONL)Y$pb-richmedia-filter fiftyone-devicedetection pb-response-correction + greenbids-real-time-data diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java index 5b87a0d4112..51196d0c2e9 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java @@ -21,6 +21,7 @@ import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; @@ -29,9 +30,19 @@ import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsSource; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode; import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.Logger; @@ -50,6 +61,8 @@ import java.time.Clock; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -110,9 +123,13 @@ public Future processEvent(T event) { return Future.succeededFuture(); } - final String greenbidsId = UUID.randomUUID().toString(); final String billingId = UUID.randomUUID().toString(); + final Map analyticsResultFromAnalyticsTag = extractAnalyticsResultFromAnalyticsTag( + auctionContext); + + final String greenbidsId = greenbidsId(analyticsResultFromAnalyticsTag); + if (!isSampled(greenbidsBidRequestExt.getGreenbidsSampling(), greenbidsId)) { return Future.succeededFuture(); } @@ -124,7 +141,8 @@ public Future processEvent(T event) { bidResponse, greenbidsId, billingId, - greenbidsBidRequestExt); + greenbidsBidRequestExt, + analyticsResultFromAnalyticsTag); commonMessageJson = jacksonMapper.encodeToString(commonMessage); } catch (PreBidException e) { return Future.failedFuture(e); @@ -162,6 +180,10 @@ private GreenbidsPrebidExt parseBidRequestExt(BidRequest bidRequest) { .orElse(null); } + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) { try { return jacksonMapper.mapper().treeToValue(adapterNode, GreenbidsPrebidExt.class); @@ -170,8 +192,62 @@ private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) { } } - private boolean isNotEmptyObjectNode(JsonNode analytics) { - return analytics != null && analytics.isObject() && !analytics.isEmpty(); + private Map extractAnalyticsResultFromAnalyticsTag(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getStageOutcomes) + .map(stages -> stages.get(Stage.processed_auction_request)) + .stream() + .flatMap(Collection::stream) + .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hook -> "greenbids-real-time-data".equals(hook.getHookId().getModuleCode())) + .map(HookExecutionOutcome::getAnalyticsTags) + .map(Tags::activities) + .flatMap(Collection::stream) + .filter(activity -> "greenbids-filter".equals(activity.name())) + .map(Activity::results) + .map(List::getFirst) + .map(Result::values) + .map(this::parseAnalyticsResult) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (existing, replacement) -> existing)); + } + + private Map parseAnalyticsResult(ObjectNode analyticsResult) { + try { + final Map parsedAnalyticsResult = new HashMap<>(); + final Iterator> fields = analyticsResult.fields(); + + while (fields.hasNext()) { + final Map.Entry field = fields.next(); + final String impId = field.getKey(); + final JsonNode explorationResultNode = field.getValue(); + final Ortb2ImpExtResult ortb2ImpExtResult = jacksonMapper.mapper() + .treeToValue(explorationResultNode, Ortb2ImpExtResult.class); + parsedAnalyticsResult.put(impId, ortb2ImpExtResult); + } + + return parsedAnalyticsResult; + } catch (JsonProcessingException e) { + throw new PreBidException("Analytics result parsing error", e); + } + } + + private String greenbidsId(Map analyticsResultFromAnalyticsTag) { + return Optional.ofNullable(analyticsResultFromAnalyticsTag) + .map(Map::values) + .map(Collection::stream) + .flatMap(Stream::findFirst) + .map(Ortb2ImpExtResult::getGreenbids) + .map(ExplorationResult::getFingerprint) + .orElseGet(() -> UUID.randomUUID().toString()); } private Future processAnalyticServerResponse(HttpClientResponse response) { @@ -213,7 +289,8 @@ private CommonMessage createBidMessage( BidResponse bidResponse, String greenbidsId, String billingId, - GreenbidsPrebidExt greenbidsImpExt) { + GreenbidsPrebidExt greenbidsImpExt, + Map analyticsResultFromAnalyticsTag) { final Optional bidRequest = Optional.ofNullable(auctionContext.getBidRequest()); final List imps = bidRequest @@ -231,8 +308,10 @@ private CommonMessage createBidMessage( final Map seatsWithNonBids = getSeatsWithNonBids(auctionContext); - final List adUnitsWithBidResponses = imps.stream().map(imp -> createAdUnit( - imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur())).toList(); + final List adUnitsWithBidResponses = imps.stream().map(imp -> + createAdUnit( + imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur(), analyticsResultFromAnalyticsTag)) + .toList(); final String auctionId = bidRequest .map(BidRequest::getId) @@ -294,7 +373,8 @@ private GreenbidsAdUnit createAdUnit( Imp imp, Map seatsWithBids, Map seatsWithNonBids, - String currency) { + String currency, + Map analyticsResultFromAnalyticsTag) { final ExtBanner extBanner = getExtBanner(imp.getBanner()); final Video video = imp.getVideo(); final Native nativeObject = imp.getXNative(); @@ -317,11 +397,17 @@ private GreenbidsAdUnit createAdUnit( final List bids = extractBidders( imp.getId(), seatsWithBids, seatsWithNonBids, impExtPrebid, currency); + final Ortb2ImpResult ortb2ImpResult = Optional.ofNullable(analyticsResultFromAnalyticsTag) + .map(analyticsResult -> analyticsResult.get(imp.getId())) + .map(Ortb2ImpResult::of) + .orElse(null); + return GreenbidsAdUnit.builder() .code(adUnitCode) .unifiedCode(greenbidsUnifiedCode) .mediaTypes(mediaTypes) .bids(bids) + .ortb2ImpResult(ortb2ImpResult) .build(); } diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java new file mode 100644 index 00000000000..48a2a0e8038 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java @@ -0,0 +1,18 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExplorationResult { + + String fingerprint; + + @JsonProperty("keptInAuction") + Map keptInAuction; + + @JsonProperty("isExploration") + Boolean isExploration; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java index ce3ae13231a..52f0ebab684 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java @@ -19,4 +19,7 @@ public class GreenbidsAdUnit { MediaTypes mediaTypes; List bids; + + @JsonProperty("ortb2Imp") + Ortb2ImpResult ortb2ImpResult; } diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java new file mode 100644 index 00000000000..c6cc8350bd8 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java @@ -0,0 +1,11 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Ortb2ImpExtResult { + + ExplorationResult greenbids; + + String tid; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java new file mode 100644 index 00000000000..377bd3c677a --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java @@ -0,0 +1,9 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Ortb2ImpResult { + + Ortb2ImpExtResult ext; +} diff --git a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java index 0e4e0e0e8fd..9167b1148ec 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java @@ -1,6 +1,7 @@ package org.prebid.server.analytics.reporter.greenbids; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Banner; @@ -26,20 +27,43 @@ import org.prebid.server.VertxTest; import org.prebid.server.analytics.model.AuctionEvent; import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsBid; import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode; import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRejectionReason; import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.v1.analytics.ResultImpl; +import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtModules; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage; +import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome; import org.prebid.server.util.HttpUtil; import org.prebid.server.version.PrebidVersionProvider; import org.prebid.server.vertx.httpclient.HttpClient; @@ -50,6 +74,7 @@ import java.time.Clock; import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -113,6 +138,65 @@ public void setUp() { prebidVersionProvider); } + @Test + public void shouldReceiveValidResponseOnAuctionContextWithAnalyticsTagForBanner() throws IOException { + // given + final Banner banner = givenBanner(); + + final ObjectNode impExtNode = mapper.createObjectNode(); + impExtNode.set("gpid", TextNode.valueOf("gpidvalue")); + impExtNode.set("prebid", givenPrebidBidderParamsNode()); + + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(impExtNode) + .banner(banner) + .build(); + + final AuctionContext auctionContext = givenAuctionContextWithAnalyticsTag( + context -> context, List.of(imp), true, true); + final AuctionEvent event = AuctionEvent.builder() + .auctionContext(auctionContext) + .bidResponse(auctionContext.getBidResponse()) + .build(); + + final HttpClientResponse mockResponse = mock(HttpClientResponse.class); + when(mockResponse.getStatusCode()).thenReturn(202); + when(httpClient.post(anyString(), any(MultiMap.class), anyString(), anyLong())) + .thenReturn(Future.succeededFuture(mockResponse)); + final CommonMessage expectedCommonMessage = givenCommonMessageForBannerWithRtb2Imp(); + + // when + final Future result = target.processEvent(event); + + // then + assertThat(result.succeeded()).isTrue(); + verify(httpClient).post( + eq(greenbidsAnalyticsProperties.getAnalyticsServerUrl()), + headersCaptor.capture(), + jsonCaptor.capture(), + eq(greenbidsAnalyticsProperties.getTimeoutMs())); + + final String capturedJson = jsonCaptor.getValue(); + final CommonMessage capturedCommonMessage = jacksonMapper.mapper() + .readValue(capturedJson, CommonMessage.class); + + assertThat(capturedCommonMessage).usingRecursiveComparison() + .ignoringFields("billingId", "greenbidsId") + .isEqualTo(expectedCommonMessage); + assertThat(capturedCommonMessage.getGreenbidsId()).isNotNull(); + assertThat(capturedCommonMessage.getBillingId()).isNotNull(); + capturedCommonMessage.getAdUnits().forEach(adUnit -> { + assertThat(adUnit.getOrtb2ImpResult().getExt().getGreenbids().getFingerprint()).isNotNull(); + assertThat(adUnit.getOrtb2ImpResult().getExt().getTid()).isNotNull(); + }); + + assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER)) + .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString()); + assertThat(headersCaptor.getValue().get(HttpUtil.CONTENT_TYPE_HEADER)) + .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString()); + } + @Test public void shouldReceiveValidResponseOnAuctionContextForBanner() throws IOException { // given @@ -508,6 +592,28 @@ private static AuctionContext givenAuctionContext( return auctionContextCustomizer.apply(auctionContextBuilder).build(); } + private static AuctionContext givenAuctionContextWithAnalyticsTag( + UnaryOperator auctionContextCustomizer, + List imps, + boolean includeBidResponse, + boolean includeHookExecutionContextWithAnalyticsTag) { + final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder() + .httpRequest(HttpRequestContext.builder().build()) + .bidRequest(givenBidRequest(request -> request, imps)) + .bidRejectionTrackers(Map.of("seat3", givenBidRejectionTracker())); + + if (includeHookExecutionContextWithAnalyticsTag) { + final HookExecutionContext hookExecutionContext = givenHookExecutionContextWithAnalyticsTag(); + auctionContextBuilder.hookExecutionContext(hookExecutionContext); + } + + if (includeBidResponse) { + auctionContextBuilder.bidResponse(givenBidResponse(response -> response)); + } + + return auctionContextCustomizer.apply(auctionContextBuilder).build(); + } + private static BidRequest givenBidRequest( UnaryOperator bidRequestCustomizer, List imps) { @@ -533,6 +639,37 @@ private static String givenUserAgent() { + "AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2"; } + private static HookExecutionContext givenHookExecutionContextWithAnalyticsTag() { + final ObjectNode analyticsResultNode = mapper.valueToTree( + singletonMap( + "adunitcodevalue", + createAnalyticsResultNode())); + + final ActivityImpl activity = ActivityImpl.of( + "greenbids-filter", + "success", + Collections.singletonList( + ResultImpl.of("success", analyticsResultNode, null))); + + final TagsImpl tags = TagsImpl.of(Collections.singletonList(activity)); + + final HookExecutionOutcome hookExecutionOutcome = HookExecutionOutcome.builder() + .hookId(HookId.of("greenbids-real-time-data", null)) + .analyticsTags(tags) + .build(); + + final GroupExecutionOutcome groupExecutionOutcome = GroupExecutionOutcome.of( + Collections.singletonList(hookExecutionOutcome)); + + final StageExecutionOutcome stageExecutionOutcome = StageExecutionOutcome.of( + "auction-request", Collections.singletonList(groupExecutionOutcome)); + + final EnumMap> stageOutcomes = new EnumMap<>(Stage.class); + stageOutcomes.put(Stage.processed_auction_request, Collections.singletonList(stageExecutionOutcome)); + + return HookExecutionContext.of(null, stageOutcomes); + } + private static BidResponse givenBidResponse(UnaryOperator bidResponseCustomizer) { return bidResponseCustomizer.apply(BidResponse.builder() .id("response1") @@ -546,10 +683,77 @@ private static BidResponse givenBidResponse(UnaryOperator bidResponseCustomizer) { + final ObjectNode analyticsResultNode = mapper.valueToTree( + singletonMap( + "adunitcodevalue", + createAnalyticsResultNode())); + + final ExtModulesTraceAnalyticsTags analyticsTags = ExtModulesTraceAnalyticsTags.of( + Collections.singletonList( + ExtModulesTraceAnalyticsActivity.of( + null, null, Collections.singletonList( + ExtModulesTraceAnalyticsResult.of( + null, analyticsResultNode, null))))); + + final ExtModulesTraceInvocationResult invocationResult = ExtModulesTraceInvocationResult.builder() + .hookId(HookId.of("greenbids-real-time-data", null)) + .analyticsTags(analyticsTags) + .build(); + + final ExtModulesTraceStageOutcome outcome = ExtModulesTraceStageOutcome.of( + "auction-request", null, + Collections.singletonList(ExtModulesTraceGroup.of( + null, Collections.singletonList(invocationResult)))); + + final ExtModulesTraceStage stage = ExtModulesTraceStage.of( + Stage.processed_auction_request, null, + Collections.singletonList(outcome)); + + final ExtModulesTrace modulesTrace = ExtModulesTrace.of(null, Collections.singletonList(stage)); + + final ExtModules modules = ExtModules.of(null, null, modulesTrace); + + final ExtBidResponsePrebid prebid = ExtBidResponsePrebid.builder().modules(modules).build(); + + final ExtBidResponse extBidResponse = ExtBidResponse.builder().prebid(prebid).build(); + + return bidResponseCustomizer.apply(BidResponse.builder() + .id("response2") + .seatbid(List.of( + givenSeatBid( + seatBid -> seatBid.seat("seat1"), + bid -> bid.id("bid1").price(BigDecimal.valueOf(1.5))), + givenSeatBid( + seatBid -> seatBid.seat("seat2"), + bid -> bid.id("bid2").price(BigDecimal.valueOf(0.5))))) + .cur("USD") + .ext(extBidResponse)).build(); + } + + private static ObjectNode createAnalyticsResultNode() { + final ObjectNode keptInAuctionNode = new ObjectNode(JsonNodeFactory.instance); + keptInAuctionNode.put("seat1", true); + keptInAuctionNode.put("seat2", true); + keptInAuctionNode.put("seat3", true); + + final ObjectNode explorationResultNode = new ObjectNode(JsonNodeFactory.instance); + explorationResultNode.put("fingerprint", "4f8d2e76-87fe-47c7-993f-d905b5fe2aa7"); + explorationResultNode.set("keptInAuction", keptInAuctionNode); + explorationResultNode.put("isExploration", false); + + final ObjectNode analyticsResultNode = new ObjectNode(JsonNodeFactory.instance); + analyticsResultNode.set("greenbids", explorationResultNode); + analyticsResultNode.put("tid", "c65c165d-f4ea-4301-bb91-982ce813dd3e"); + + return analyticsResultNode; + } + private static SeatBid givenSeatBid(UnaryOperator seatBidCostumizer, UnaryOperator... bidCustomizers) { return seatBidCostumizer.apply(SeatBid.builder() - .bid(givenBids(bidCustomizers))).build(); + .bid(givenBids(bidCustomizers))).build(); } private static List givenBids(UnaryOperator... bidCustomizers) { @@ -621,6 +825,16 @@ private static CommonMessage expectedCommonMessageForBanner() { .bids(expectedGreenbidBids())); } + private static CommonMessage givenCommonMessageForBannerWithRtb2Imp() { + return expectedCommonMessage( + adUnit -> adUnit + .code("adunitcodevalue") + .unifiedCode(GreenbidsUnifiedCode.of("gpidvalue", "gpidSource")) + .mediaTypes(MediaTypes.of(givenExtBanner(320, 50, null), null, null)) + .bids(expectedGreenbidBids()) + .ortb2ImpResult(givenOrtb2Imp())); + } + private static CommonMessage expectedCommonMessageForVideo() { return expectedCommonMessage( adUnit -> adUnit @@ -718,4 +932,17 @@ private static Video givenVideo() { .plcmt(1) .build(); } + + private static Ortb2ImpResult givenOrtb2Imp() { + return Ortb2ImpResult.of( + Ortb2ImpExtResult.of( + ExplorationResult.of( + "4f8d2e76-87fe-47c7-993f-d905b5fe2aa7", + Map.of("seat1", true, "seat2", true, "seat3", true), + false + ), + "c65c165d-f4ea-4301-bb91-982ce813dd3e" + ) + ); + } } From 6af31f53598e990f5bfa8990c350bdbb01c4aaf4 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:33:17 +0100 Subject: [PATCH 122/170] Module Execution: Add Property to Require Account Config (#3525) --- docs/metrics.md | 12 + ...ConfiantAdQualityBidResponsesScanHook.java | 2 +- .../v1/model/InvocationResultImpl.java | 36 --- ...FiftyOneDeviceDetectionEntrypointHook.java | 2 +- ...eDeviceDetectionRawAuctionRequestHook.java | 2 +- .../v1/model/InvocationResultImpl.java | 24 -- .../v1/Ortb2BlockingBidderRequestHook.java | 2 +- .../Ortb2BlockingRawBidderResponseHook.java | 2 +- .../v1/model/InvocationResultImpl.java | 37 --- .../Ortb2BlockingBidderRequestHookTest.java | 2 +- ...rtb2BlockingRawBidderResponseHookTest.java | 2 +- ...orrectionAllProcessedBidResponsesHook.java | 8 +- .../v1/model/InvocationResultImpl.java | 38 --- ...diaFilterAllProcessedBidResponsesHook.java | 2 +- .../server/auction/ExchangeService.java | 1 + .../server/hooks/execution/GroupExecutor.java | 32 ++- .../server/hooks/execution/GroupResult.java | 1 + .../hooks/execution/HookStageExecutor.java | 13 +- .../server/hooks/execution/StageExecutor.java | 11 +- .../execution/model/ExecutionAction.java | 2 +- .../server/hooks/v1/InvocationAction.java | 2 +- .../hooks/v1}/InvocationResultImpl.java | 6 +- .../org/prebid/server/metric/MetricName.java | 1 + .../org/prebid/server/metric/Metrics.java | 17 +- .../prebid/server/metric/StageMetrics.java | 1 + .../spring/config/HooksConfiguration.java | 8 +- .../model/config/EndpointExecutionPlan.groovy | 12 + .../model/config/ExecutionPlan.groovy | 4 + .../functional/model/config/Stage.groovy | 20 +- .../response/auction/ResponseAction.groovy | 2 +- .../tests/module/GeneralModuleSpec.groovy | 240 ++++++++++++++++++ .../tests/module/ModuleBaseSpec.groovy | 6 +- .../execution/HookStageExecutorTest.java | 206 ++++++++++----- ...ltImpl.java => InvocationResultUtils.java} | 30 +-- .../it/hooks/SampleItAuctionResponseHook.java | 4 +- .../it/hooks/SampleItBidderRequestHook.java | 4 +- .../it/hooks/SampleItEntrypointHook.java | 6 +- .../SampleItProcessedAuctionRequestHook.java | 4 +- .../SampleItProcessedBidderResponseHook.java | 4 +- .../hooks/SampleItRawBidderResponseHook.java | 4 +- .../SampleItRejectingBidderRequestHook.java | 4 +- ...tRejectingProcessedAuctionRequestHook.java | 4 +- ...tRejectingProcessedBidderResponseHook.java | 4 +- ...ampleItRejectingRawAuctionRequestHook.java | 4 +- ...ampleItRejectingRawBidderResponseHook.java | 4 +- .../org/prebid/server/metric/MetricsTest.java | 27 +- 46 files changed, 552 insertions(+), 307 deletions(-) delete mode 100644 extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java delete mode 100644 extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java delete mode 100644 extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java delete mode 100644 extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java rename {extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model => src/main/java/org/prebid/server/hooks/v1}/InvocationResultImpl.java (66%) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy rename src/test/java/org/prebid/server/hooks/v1/{InvocationResultImpl.java => InvocationResultUtils.java} (75%) diff --git a/docs/metrics.md b/docs/metrics.md index 11f2165978c..504cd50f5db 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -133,3 +133,15 @@ Following metrics are collected and submitted if account is configured with `det - `analytics..(auction|amp|video|cookie_sync|event|setuid).timeout` - number of event requests, failed with timeout cause - `analytics..(auction|amp|video|cookie_sync|event|setuid).err` - number of event requests, failed with errors - `analytics..(auction|amp|video|cookie_sync|event|setuid).badinput` - number of event requests, rejected with bad input cause + +## Modules metrics +- `modules.module..stage..hook..call` - number of times the hook is called +- `modules.module..stage..hook..duration` - timer tracking the called hook execution time +- `modules.module..stage..hook..success.(noop|update|reject|no-invocation)` - number of times the hook is called successfully with the action applied +- `modules.module..stage..hook..(failure|timeout|execution-error)` - number of times the hook execution is failed + +## Modules per-account metrics +- `account..modules.module..call` - number of times the module is called +- `account..modules.module..duration` - timer tracking the called module execution time +- `account..modules.module..success.(noop|update|reject|no-invocation)` - number of times the module is called successfully with the action applied +- `account..modules.module..failure` - number of times the module execution is failed diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java index 7db1446bcce..2db501f1852 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java @@ -18,9 +18,9 @@ import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner; import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java deleted file mode 100644 index 76fa5759644..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java index 9df4e2a0237..506cf66078e 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java @@ -2,10 +2,10 @@ import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence; import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext; -import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java index 081177e8ca1..8b0d5cdc8d4 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java @@ -13,9 +13,9 @@ import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.EnrichmentResult; import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.SecureHeadersRetriever; import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext; -import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java deleted file mode 100644 index ead75085974..00000000000 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model; - -import lombok.Builder; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Builder -public record InvocationResultImpl( - InvocationStatus status, - String message, - InvocationAction action, - PayloadUpdate payloadUpdate, - List errors, - List warnings, - List debugMessages, - Object moduleContext, - Tags analyticsTags -) implements InvocationResult { -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java index ff9e6ab6c3c..bb5e05d4fb2 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java @@ -11,9 +11,9 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java index 329e99d3bbc..c90ac94840a 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java @@ -13,13 +13,13 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java deleted file mode 100644 index 48be15fdf37..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - ModuleContext moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index fe1ea6e9614..4c3852bc630 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -28,9 +28,9 @@ import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java index 0e0a6811835..356bd7b2125 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java @@ -23,13 +23,13 @@ import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java index 09e4640b064..4d7fb81c366 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -11,9 +11,9 @@ import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider; import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config; import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; -import org.prebid.server.hooks.modules.pb.response.correction.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; @@ -57,7 +57,7 @@ public Future> call(AllProcess return noAction(); } - final InvocationResult invocationResult = InvocationResultImpl.builder() + final InvocationResult invocationResult = InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) .payloadUpdate(initialPayload -> AllProcessedBidResponsesPayloadImpl.of( @@ -84,7 +84,7 @@ private static List applyCorrections(List bidder } private Future> failure(String message) { - return Future.succeededFuture(InvocationResultImpl.builder() + return Future.succeededFuture(InvocationResultImpl.builder() .status(InvocationStatus.failure) .message(message) .action(InvocationAction.no_action) @@ -92,7 +92,7 @@ private Future> failure(String } private static Future> noAction() { - return Future.succeededFuture(InvocationResultImpl.builder() + return Future.succeededFuture(InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.no_action) .build()); diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java deleted file mode 100644 index 1a39413583c..00000000000 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.prebid.server.hooks.modules.pb.response.correction.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; -import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java index ee92a5e2064..e0416c6d30d 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java @@ -12,13 +12,13 @@ import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.InvocationResultImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 438a0f71acd..27c65afe260 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -1391,6 +1391,7 @@ private AuctionContext updateHooksMetrics(AuctionContext context) { .flatMap(Collection::stream) .map(GroupExecutionOutcome::getHooks) .flatMap(Collection::stream) + .filter(hookOutcome -> hookOutcome.getAction() != ExecutionAction.no_invocation) .collect(Collectors.groupingBy( outcome -> outcome.getHookId().getModuleCode(), Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java index 2525651f872..5fac3d8376b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.execution; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Promise; @@ -8,8 +9,12 @@ import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.LoggerFactory; @@ -25,6 +30,7 @@ class GroupExecutor { private final Vertx vertx; private final Clock clock; + private final boolean isConfigToInvokeRequired; private ExecutionGroup group; private PAYLOAD initialPayload; @@ -33,16 +39,18 @@ class GroupExecutor { private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; - private GroupExecutor(Vertx vertx, Clock clock) { + private GroupExecutor(Vertx vertx, Clock clock, boolean isConfigToInvokeRequired) { this.vertx = vertx; this.clock = clock; + this.isConfigToInvokeRequired = isConfigToInvokeRequired; } public static GroupExecutor create( Vertx vertx, - Clock clock) { + Clock clock, + boolean isConfigToInvokeRequired) { - return new GroupExecutor<>(vertx, clock); + return new GroupExecutor<>(vertx, clock, isConfigToInvokeRequired); } public GroupExecutor withGroup(ExecutionGroup group) { @@ -107,11 +115,19 @@ private Future> executeHook( return Future.failedFuture(new FailedException("Hook implementation does not exist or disabled")); } - return executeWithTimeout( - () -> hook.call( - groupResult.payload(), - invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId))), - timeout); + final CONTEXT invocationContext = invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId)); + + if (isConfigToInvokeRequired && invocationContext instanceof AuctionInvocationContext) { + final ObjectNode accountConfig = ((AuctionInvocationContext) invocationContext).accountConfig(); + if (accountConfig == null || accountConfig.isNull()) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_invocation) + .build()); + } + } + + return executeWithTimeout(() -> hook.call(groupResult.payload(), invocationContext), timeout); } private Future executeWithTimeout(Supplier> action, Long timeout) { diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java index a4487e3a60b..8bc7c5c0723 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java @@ -173,6 +173,7 @@ private static ExecutionAction toExecutionAction(InvocationAction action) { case reject -> ExecutionAction.reject; case update -> ExecutionAction.update; case no_action -> ExecutionAction.no_action; + case no_invocation -> ExecutionAction.no_invocation; }; } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java index 81f44e3a528..ce5a8df813a 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -70,13 +70,15 @@ public class HookStageExecutor { private final TimeoutFactory timeoutFactory; private final Vertx vertx; private final Clock clock; + private final boolean isConfigToInvokeRequired; private HookStageExecutor(ExecutionPlan hostExecutionPlan, ExecutionPlan defaultAccountExecutionPlan, HookCatalog hookCatalog, TimeoutFactory timeoutFactory, Vertx vertx, - Clock clock) { + Clock clock, + boolean isConfigToInvokeRequired) { this.hostExecutionPlan = hostExecutionPlan; this.defaultAccountExecutionPlan = defaultAccountExecutionPlan; @@ -84,6 +86,7 @@ private HookStageExecutor(ExecutionPlan hostExecutionPlan, this.timeoutFactory = timeoutFactory; this.vertx = vertx; this.clock = clock; + this.isConfigToInvokeRequired = isConfigToInvokeRequired; } public static HookStageExecutor create(String hostExecutionPlan, @@ -92,7 +95,8 @@ public static HookStageExecutor create(String hostExecutionPlan, TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + boolean isConfigToInvokeRequired) { return new HookStageExecutor( parseAndValidateExecutionPlan( @@ -103,7 +107,8 @@ public static HookStageExecutor create(String hostExecutionPlan, hookCatalog, Objects.requireNonNull(timeoutFactory), Objects.requireNonNull(vertx), - Objects.requireNonNull(clock)); + Objects.requireNonNull(clock), + isConfigToInvokeRequired); } public Future> executeEntrypointStage( @@ -254,7 +259,7 @@ private StageExecutorcreate(hookCatalog, vertx, clock) + return StageExecutor.create(hookCatalog, vertx, clock, isConfigToInvokeRequired) .withStage(stage) .withEntity(entity) .withHookExecutionContext(context); diff --git a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java index f4f4a8176de..d99cd14030f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java @@ -18,6 +18,7 @@ class StageExecutor { private final HookCatalog hookCatalog; private final Vertx vertx; private final Clock clock; + private final boolean isConfigToInvokeRequired; private StageWithHookType> stage; private String entity; @@ -27,18 +28,20 @@ class StageExecutor { private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; - private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) { + private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock, boolean isConfigToInvokeRequired) { this.hookCatalog = hookCatalog; this.vertx = vertx; this.clock = clock; + this.isConfigToInvokeRequired = isConfigToInvokeRequired; } public static StageExecutor create( HookCatalog hookCatalog, Vertx vertx, - Clock clock) { + Clock clock, + boolean isConfigToInvokeRequired) { - return new StageExecutor<>(hookCatalog, vertx, clock); + return new StageExecutor<>(hookCatalog, vertx, clock, isConfigToInvokeRequired); } public StageExecutor withStage(StageWithHookType> stage) { @@ -94,7 +97,7 @@ public Future> execute() { } private Future> executeGroup(ExecutionGroup group, PAYLOAD initialPayload) { - return GroupExecutor.create(vertx, clock) + return GroupExecutor.create(vertx, clock, isConfigToInvokeRequired) .withGroup(group) .withInitialPayload(initialPayload) .withHookProvider( diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java index 5e13aa3f14c..886cec114e8 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java @@ -2,5 +2,5 @@ public enum ExecutionAction { - no_action, update, reject + no_action, update, reject, no_invocation } diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java index 29b22bf1b3d..821b21a730b 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java @@ -2,5 +2,5 @@ public enum InvocationAction { - no_action, update, reject + no_action, update, reject, no_invocation } diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java b/src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java similarity index 66% rename from extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java rename to src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java index b77fc98a68b..abfe5cf8fb2 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java @@ -1,12 +1,8 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model; +package org.prebid.server.hooks.v1; import lombok.Builder; import lombok.Value; import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.analytics.Tags; import java.util.List; diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index fc9d3c251c3..7f27ea2212e 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -138,6 +138,7 @@ public enum MetricName { call, success, noop, + no_invocation("no-invocation"), reject, unknown, failure, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index ed11d511f5f..c435e361fec 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -614,13 +614,20 @@ public void updateHooksMetrics( final HookImplMetrics hookImplMetrics = hooks().module(moduleCode).stage(stage).hookImpl(hookImplCode); - hookImplMetrics.incCounter(MetricName.call); + if (action != ExecutionAction.no_invocation) { + hookImplMetrics.incCounter(MetricName.call); + } + if (status == ExecutionStatus.success) { hookImplMetrics.success().incCounter(HookMetricMapper.fromAction(action)); } else { hookImplMetrics.incCounter(HookMetricMapper.fromStatus(status)); } - hookImplMetrics.updateTimer(MetricName.duration, executionTime); + + if (action != ExecutionAction.no_invocation) { + hookImplMetrics.updateTimer(MetricName.duration, executionTime); + } + } public void updateAccountHooksMetrics( @@ -632,7 +639,10 @@ public void updateAccountHooksMetrics( if (accountMetricsVerbosityResolver.forAccount(account).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { final ModuleMetrics accountModuleMetrics = forAccount(account.getId()).hooks().module(moduleCode); - accountModuleMetrics.incCounter(MetricName.call); + if (action != ExecutionAction.no_invocation) { + accountModuleMetrics.incCounter(MetricName.call); + } + if (status == ExecutionStatus.success) { accountModuleMetrics.success().incCounter(HookMetricMapper.fromAction(action)); } else { @@ -663,6 +673,7 @@ private static class HookMetricMapper { ACTION_TO_METRIC.put(ExecutionAction.no_action, MetricName.noop); ACTION_TO_METRIC.put(ExecutionAction.update, MetricName.update); ACTION_TO_METRIC.put(ExecutionAction.reject, MetricName.reject); + ACTION_TO_METRIC.put(ExecutionAction.no_invocation, MetricName.no_invocation); } static MetricName fromStatus(ExecutionStatus status) { diff --git a/src/main/java/org/prebid/server/metric/StageMetrics.java b/src/main/java/org/prebid/server/metric/StageMetrics.java index 1cc8f3adfb3..1348266b0f7 100644 --- a/src/main/java/org/prebid/server/metric/StageMetrics.java +++ b/src/main/java/org/prebid/server/metric/StageMetrics.java @@ -21,6 +21,7 @@ class StageMetrics extends UpdatableMetrics { STAGE_TO_METRIC.put(Stage.raw_bidder_response, "rawbidresponse"); STAGE_TO_METRIC.put(Stage.processed_bidder_response, "procbidresponse"); STAGE_TO_METRIC.put(Stage.auction_response, "auctionresponse"); + STAGE_TO_METRIC.put(Stage.all_processed_bid_responses, "allprocbidresponses"); } private static final String UNKNOWN_STAGE = "unknown"; diff --git a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java index bffc5ee32f0..6b77be58153 100644 --- a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java @@ -8,6 +8,7 @@ import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.v1.Module; import org.prebid.server.json.JacksonMapper; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,7 +31,9 @@ HookStageExecutor hookStageExecutor(HooksConfigurationProperties hooksConfigurat TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + @Value("${settings.modules.require-config-to-invoke:false}") + boolean isConfigToInvokeRequired) { return HookStageExecutor.create( hooksConfiguration.getHostExecutionPlan(), @@ -39,7 +42,8 @@ HookStageExecutor hookStageExecutor(HooksConfigurationProperties hooksConfigurat timeoutFactory, vertx, clock, - mapper); + mapper, + isConfigToInvokeRequired); } @Bean diff --git a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy index b73f4fcaeb3..9ded40849d0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy @@ -12,4 +12,16 @@ class EndpointExecutionPlan { new EndpointExecutionPlan(stages: stages.collectEntries { it -> [(it): StageExecutionPlan.getModuleStageExecutionPlan(name, it)] } as Map) } + + static EndpointExecutionPlan getModulesEndpointExecutionPlan(Map> modulesStages) { + new EndpointExecutionPlan( + stages: modulesStages.collectEntries { stage, moduleNames -> + [(stage): new StageExecutionPlan( + groups: moduleNames.collect { moduleName -> + ExecutionGroup.getModuleExecutionGroup(moduleName, stage) + } + )] + } as Map + ) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy index 766139bc5c5..9e5cfc3ac93 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy @@ -11,4 +11,8 @@ class ExecutionPlan { static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, ModuleName moduleName, List stage) { new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModuleEndpointExecutionPlan(moduleName, stage)]) } + + static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, Map> modulesStages) { + new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModulesEndpointExecutionPlan(modulesStages)]) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy index c77fd9ebcda..178f22552ae 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy @@ -6,20 +6,22 @@ import groovy.transform.ToString @ToString enum Stage { - ENTRYPOINT("entrypoint"), - RAW_AUCTION_REQUEST("raw-auction-request"), - PROCESSED_AUCTION_REQUEST("processed-auction-request"), - BIDDER_REQUEST("bidder-request"), - RAW_BIDDER_RESPONSE("raw-bidder-response"), - PROCESSED_BIDDER_RESPONSE("processed-bidder-response"), - ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses"), - AUCTION_RESPONSE("auction-response") + ENTRYPOINT("entrypoint", "entrypoint"), + RAW_AUCTION_REQUEST("raw-auction-request", "rawauction"), + PROCESSED_AUCTION_REQUEST("processed-auction-request", "procauction"), + BIDDER_REQUEST("bidder-request", "bidrequest"), + RAW_BIDDER_RESPONSE("raw-bidder-response", "rawbidresponse"), + PROCESSED_BIDDER_RESPONSE("processed-bidder-response", "procbidresponse"), + ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses", "allprocbidresponses"), + AUCTION_RESPONSE("auction-response", "auctionresponse") @JsonValue final String value + final String metricValue - Stage(String value) { + Stage(String value, String metricValue) { this.value = value + this.metricValue = metricValue } @Override diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy index 1a786670ba8..1bce783d048 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum ResponseAction { - UPDATE, NO_ACTION + UPDATE, NO_ACTION, NO_INVOCATION @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy new file mode 100644 index 00000000000..fa2929665a6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy @@ -0,0 +1,240 @@ +package org.prebid.server.functional.tests.module + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.RichmediaFilter +import org.prebid.server.functional.model.request.auction.TraceLevel +import org.prebid.server.functional.model.response.auction.InvocationResult +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION +import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES +import static org.prebid.server.functional.model.config.ModuleHookImplementation.RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_INVOCATION + +class GeneralModuleSpec extends ModuleBaseSpec { + + private final static String NO_INVOCATION_METRIC = "modules.module.%s.stage.%s.hook.%s.success.no-invocation" + private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call" + + private final static Map DISABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'false'] + private final static Map ENABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'true'] + private final static Map MULTY_MODULE_CONFIG = getRichMediaFilterSettings(PBSUtils.randomString) + getResponseCorrectionConfig() + + ['hooks.host-execution-plan': encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION]]))] + + private final static PrebidServerService pbsServiceWithMultipleModule = pbsServiceFactory.getService(MULTY_MODULE_CONFIG + DISABLED_INVOKE_CONFIG) + private final static PrebidServerService pbsServiceWithMultipleModuleWithRequireInvoke = pbsServiceFactory.getService(MULTY_MODULE_CONFIG + ENABLED_INVOKE_CONFIG) + + def "PBS should call all modules and traces response when account config is empty and require-config-to-invoke is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModule) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + } + + and: "no-invocation metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + and: "hook call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, pbResponseCorrection: pbResponseCorrectionConfig) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModule) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + } + + and: "no-invocation metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + and: "hook call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + pbRichmediaFilterConfig | pbResponseCorrectionConfig + new RichmediaFilter() | new PbResponseCorrection() + new RichmediaFilter() | new PbResponseCorrection(enabled: false) + new RichmediaFilter() | new PbResponseCorrection(enabled: true) + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection() + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true) + } + + def "PBS shouldn't call any modules and traces that in response when account config is empty and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about no-called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + } + + and: "no-invocation metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + and: "hook call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account with enabled response correction module" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, pbResponseCorrection: pbResponseCorrectionConfig) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + } + + and: "no-invocation metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + and: "hook call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + pbRichmediaFilterConfig | pbResponseCorrectionConfig + new RichmediaFilter() | new PbResponseCorrection() + new RichmediaFilter() | new PbResponseCorrection(enabled: false) + new RichmediaFilter() | new PbResponseCorrection(enabled: true) + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection() + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true) + } + + def "PBS should call specified module and traces response when account config includes that module and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account with enabled response correction module" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection()))) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called module" + def invocationTrace = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RESPONSE_CORRECTION.code }) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RESPONSE_CORRECTION].code.sort() + } + + and: "PBS response should include trace information about no-called module" + verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RICHMEDIA_FILTER.code }) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER].code.sort() + } + + and: "Richmedia module metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + and: "Response-correction module metrics should be updated" + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 19cb2cd53de..9cd310be252 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -24,8 +24,8 @@ class ModuleBaseSpec extends BaseSpec { } protected static Map getResponseCorrectionConfig(Endpoint endpoint = OPENRTB2_AUCTION) { - ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled" : true, - "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RESPONSE_CORRECTION, [ALL_PROCESSED_BID_RESPONSES]))] + ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled": true, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]]))] .collectEntries { key, value -> [(key.toString()): value.toString()] } } @@ -36,7 +36,7 @@ class ModuleBaseSpec extends BaseSpec { ["hooks.${PB_RICHMEDIA_FILTER.code}.enabled" : true, "hooks.modules.${PB_RICHMEDIA_FILTER.code}.mraid-script-pattern": scriptPattern, "hooks.modules.${PB_RICHMEDIA_FILTER.code}.filter-mraid" : filterMraidEnabled, - "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]))] + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]]))] .collectEntries { key, value -> [(key.toString()): value.toString()] } } diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java index 060855334cc..8b2358b9708 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -53,6 +53,7 @@ import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.analytics.ActivityImpl; import org.prebid.server.hooks.v1.analytics.AppliedToImpl; @@ -152,7 +153,7 @@ public void creationShouldFailWhenHostExecutionPlanHasUnknownHook() { given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(null); - givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction())); + givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction())); assertThatThrownBy(() -> createExecutor(hostPlan)) .isInstanceOf(IllegalArgumentException.class) @@ -175,7 +176,7 @@ public void creationShouldFailWhenDefaultAccountExecutionPlanHasUnknownHook() { given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(null); - givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction())); + givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction())); assertThatThrownBy(() -> createExecutor(null, defaultAccountPlan)) .isInstanceOf(IllegalArgumentException.class) @@ -233,7 +234,7 @@ public void shouldExecuteEntrypointHooksHappyPath(VertxTestContext context) { givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded( + immediateHook(InvocationResultUtils.succeeded( payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-abc"), "moduleAlphaContext"))); @@ -241,19 +242,19 @@ public void shouldExecuteEntrypointHooksHappyPath(VertxTestContext context) { givenEntrypointHook( "module-alpha", "hook-b", - delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-def")), 40)); givenEntrypointHook( "module-beta", "hook-a", - delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 80)); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded( + immediateHook(InvocationResultUtils.succeeded( payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl"), "moduleBetaContext"))); @@ -427,7 +428,7 @@ public void shouldExecuteEntrypointHooksToleratingMisbehavingHooks(VertxTestCont givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookStageExecutor executor = createExecutor( @@ -523,7 +524,7 @@ public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(VertxTe "module-alpha", "hook-b", delayedHook( - InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-def")), 250)); @@ -532,14 +533,14 @@ public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(VertxTe "module-beta", "hook-a", delayedHook( - InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 250)); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); @@ -622,23 +623,23 @@ public void shouldExecuteEntrypointHooksHonoringStatusAndAction(VertxTestContext givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.failed("Failed to contact service ACME"))); + immediateHook(InvocationResultUtils.failed("Failed to contact service ACME"))); givenEntrypointHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.noAction())); + immediateHook(InvocationResultUtils.noAction())); givenEntrypointHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-ghi")))); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookStageExecutor executor = createExecutor( @@ -714,13 +715,13 @@ public void shouldExecuteEntrypointHooksWhenRequestIsRejectedByFirstGroup(VertxT givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-abc")))); givenEntrypointHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is of low quality"))); + immediateHook(InvocationResultUtils.rejected("Request is of low quality"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -786,24 +787,24 @@ public void shouldExecuteEntrypointHooksWhenRequestIsRejectedBySecondGroup(Vertx givenEntrypointHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-abc")))); givenEntrypointHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.rejected("Request is of low quality"))); + immediateHook(InvocationResultUtils.rejected("Request is of low quality"))); givenEntrypointHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-def")))); givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), payload.headers(), payload.body() + "-jkl")))); final HookStageExecutor executor = createExecutor( @@ -902,7 +903,7 @@ public void shouldExecuteEntrypointHooksToleratingMisbehavingInvocationResult(Ve givenEntrypointHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> { + immediateHook(InvocationResultUtils.succeeded(payload -> { throw new RuntimeException("Can not alter payload"); }))); @@ -1044,7 +1045,7 @@ public void shouldExecuteEntrypointHooksAndStoreResultInExecutionContext(VertxTe public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContext context) { // given final EntrypointHookImpl hookImpl = spy( - EntrypointHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + EntrypointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) @@ -1107,7 +1108,7 @@ public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContex public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(VertxTestContext context) { // given final RawAuctionRequestHookImpl hookImpl = spy( - RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction()))); + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction()))); given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); @@ -1152,7 +1153,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan(VertxTestContext context) { // given final RawAuctionRequestHookImpl hookImpl = spy( - RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction()))); + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction()))); given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); @@ -1215,7 +1216,7 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla givenRawAuctionRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); final HookStageExecutor executor = createExecutor(null, null); @@ -1287,31 +1288,105 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla })); } + @Test + public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsRequiredButAbsent(VertxTestContext context) { + // given + givenRawAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + + givenRawAuctionRequestHook( + "module-alpha", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenRawAuctionRequestHook( + "module-beta", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenRawAuctionRequestHook( + "module-beta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + final String hostExecutionPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap( + Stage.raw_auction_request, + execPlanTwoGroupsTwoHooksEach())))); + + final HookStageExecutor executor = HookStageExecutor.create( + hostExecutionPlan, + null, + hookCatalog, + timeoutFactory, + vertx, + clock, + jacksonMapper, + true); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final BidRequest givenBidRequest = BidRequest.builder().build(); + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(givenBidRequest) + .account(Account.empty("accountId")) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull() + .extracting(AuctionRequestPayload::bidRequest) + .isEqualTo(givenBidRequest); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("auction-request")); + + context.completeNow(); + })); + } + @Test public void shouldExecuteRawAuctionRequestHooksHappyPath(VertxTestContext context) { // given givenRawAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().at(1).build())))); givenRawAuctionRequestHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); givenRawAuctionRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().test(1).build())))); givenRawAuctionRequestHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); final HookStageExecutor executor = createExecutor( @@ -1359,7 +1434,7 @@ public void shouldExecuteRawAuctionRequestHooksHappyPath(VertxTestContext contex public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final RawAuctionRequestHookImpl hookImpl = spy( - RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) @@ -1532,7 +1607,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenRequestIsRejected(VertxTestCo givenRawAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is no good"))); + immediateHook(InvocationResultUtils.rejected("Request is no good"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -1565,25 +1640,25 @@ public void shouldExecuteProcessedAuctionRequestHooksHappyPath(VertxTestContext givenProcessedAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().at(1).build())))); givenProcessedAuctionRequestHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); givenProcessedAuctionRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().test(1).build())))); givenProcessedAuctionRequestHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); final HookStageExecutor executor = createExecutor( @@ -1632,7 +1707,7 @@ public void shouldExecuteProcessedAuctionRequestHooksHappyPath(VertxTestContext public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final ProcessedAuctionRequestHookImpl hookImpl = spy( - ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) @@ -1807,7 +1882,7 @@ public void shouldExecuteProcessedAuctionRequestHooksWhenRequestIsRejected(Vertx givenProcessedAuctionRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is no good"))); + immediateHook(InvocationResultUtils.rejected("Request is no good"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -1843,25 +1918,25 @@ public void shouldExecuteBidderRequestHooksHappyPath(VertxTestContext context) { givenBidderRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().at(1).build())))); givenBidderRequestHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); givenBidderRequestHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().test(1).build())))); givenBidderRequestHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); final HookStageExecutor executor = createExecutor( @@ -1928,7 +2003,7 @@ public void shouldExecuteBidderRequestHooksHappyPath(VertxTestContext context) { public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxTestContext context) { // given final BidderRequestHookImpl hookImpl = spy( - BidderRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + BidderRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.BIDDER_REQUEST))) .willReturn(hookImpl); @@ -1979,7 +2054,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().id("bidId").build(), @@ -1990,7 +2065,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adid("adId").build(), @@ -2001,7 +2076,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().cid("cid").build(), @@ -2012,7 +2087,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex givenRawBidderResponseHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adm("adm").build(), @@ -2083,7 +2158,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(VertxTestContext context) { // given final RawBidderResponseHookImpl hookImpl = spy( - RawBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + RawBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) .willReturn(hookImpl); @@ -2134,7 +2209,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().id("bidId").build(), @@ -2145,7 +2220,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adid("adId").build(), @@ -2156,7 +2231,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().cid("cid").build(), @@ -2167,7 +2242,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext givenProcessedBidderResponseHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of( payload.bids().stream() .map(bid -> BidderBid.of( bid.getBid().toBuilder().adm("adm").build(), @@ -2241,7 +2316,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationContext(VertxTestContext context) { // given final ProcessedBidderResponseHookImpl hookImpl = spy( - ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) .willReturn(hookImpl); @@ -2305,7 +2380,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2317,7 +2392,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2329,7 +2404,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2341,7 +2416,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { givenAllProcessedBidderResponsesHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of( payload.bidResponses().stream() .map(bidModifierForResponse.apply( (bidder, bid) -> BidderBid.of( @@ -2406,7 +2481,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() { public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final AllProcessedBidResponsesHookImpl hookImpl = spy( - AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) .willReturn(hookImpl); @@ -2459,7 +2534,7 @@ public void shouldExecuteBidderRequestHooksWhenRequestIsRejected(VertxTestContex givenBidderRequestHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Request is no good"))); + immediateHook(InvocationResultUtils.rejected("Request is no good"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -2495,25 +2570,25 @@ public void shouldExecuteAuctionResponseHooksHappyPath(VertxTestContext context) givenAuctionResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().id("id").build())))); givenAuctionResponseHook( "module-alpha", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().bidid("bidid").build())))); givenAuctionResponseHook( "module-beta", "hook-a", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().cur("cur").build())))); givenAuctionResponseHook( "module-beta", "hook-b", - immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of( + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of( payload.bidResponse().toBuilder().nbr(1).build())))); final HookStageExecutor executor = createExecutor( @@ -2554,7 +2629,7 @@ public void shouldExecuteAuctionResponseHooksHappyPath(VertxTestContext context) public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(VertxTestContext context) { // given final AuctionResponseHookImpl hookImpl = spy( - AuctionResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity())))); + AuctionResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) .willReturn(hookImpl); @@ -2601,7 +2676,7 @@ public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext givenAuctionResponseHook( "module-alpha", "hook-a", - immediateHook(InvocationResultImpl.rejected("Will not apply"))); + immediateHook(InvocationResultUtils.rejected("Will not apply"))); final HookStageExecutor executor = createExecutor( executionPlan(singletonMap( @@ -2798,7 +2873,8 @@ private HookStageExecutor createExecutor(String hostExecutionPlan, String defaul timeoutFactory, vertx, clock, - jacksonMapper); + jacksonMapper, + false); } @Value(staticConstructor = "of") diff --git a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java similarity index 75% rename from src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java rename to src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java index 31426173e9c..ce3e9ca74cb 100644 --- a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java +++ b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java @@ -1,34 +1,10 @@ package org.prebid.server.hooks.v1; -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Tags; +public class InvocationResultUtils { -import java.util.List; + private InvocationResultUtils() { -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; + } public static InvocationResult succeeded(PayloadUpdate payloadUpdate) { return InvocationResultImpl.builder() diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java index 360e61fae47..8073a5edc27 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java @@ -4,7 +4,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; @@ -19,7 +19,7 @@ public Future> call( final BidResponse updatedBidResponse = updateBidResponse(originalBidResponse); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of(payload.bidResponse().toBuilder() .seatbid(updatedBidResponse.getSeatbid()) .build()))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java index 10af73e4d4f..4c95bcb5a7f 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java @@ -5,7 +5,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; @@ -22,7 +22,7 @@ public Future> call( final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of(payload.bidRequest().toBuilder() .imp(updatedBidRequest.getImp()) .build()))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java index 36827bd39df..a4011db9106 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java @@ -5,7 +5,7 @@ import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -18,7 +18,7 @@ public Future> call( final boolean rejectFlag = Boolean.parseBoolean(entrypointPayload.queryParams().get("sample-it-module-reject")); if (rejectFlag) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by sample entrypoint hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by sample entrypoint hook")); } return maybeUpdate(entrypointPayload); @@ -35,7 +35,7 @@ private Future> maybeUpdate(EntrypointPayloa ? updateBody(entrypointPayload.body()) : entrypointPayload.body(); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of( + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of( payload.queryParams(), updatedHeaders, updatedBody))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java index a285235f420..dca19dd6043 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java @@ -6,7 +6,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; @@ -29,7 +29,7 @@ public Future> call( final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(payload.bidRequest().toBuilder() .ext(updatedBidRequest.getExt()) .build()))); diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java index 3f8e9ee7ae2..b626e03d5ac 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java @@ -4,7 +4,7 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; @@ -21,7 +21,7 @@ public Future> call( final List updatedBids = updateBids(originalBids); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(updatedBids))); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java index fb6d915717e..0f30527519b 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java @@ -4,7 +4,7 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; @@ -21,7 +21,7 @@ public Future> call( final List updatedBids = updateBids(originalBids); - return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> + return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(updatedBids))); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java index bd90a974936..d08303b93ce 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; @@ -13,7 +13,7 @@ public class SampleItRejectingBidderRequestHook implements BidderRequestHook { public Future> call( BidderRequestPayload bidderRequestPayload, BidderInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting bidder request hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting bidder request hook")); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java index b5feb3aaef9..5dfad73d026 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; @@ -13,7 +13,7 @@ public class SampleItRejectingProcessedAuctionRequestHook implements ProcessedAu public Future> call( AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected( + return Future.succeededFuture(InvocationResultUtils.rejected( "Rejected by rejecting processed auction request hook")); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java index a6f1438402c..d2c568837ca 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; @@ -13,7 +13,7 @@ public class SampleItRejectingProcessedBidderResponseHook implements ProcessedBi public Future> call( BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected( + return Future.succeededFuture(InvocationResultUtils.rejected( "Rejected by rejecting processed bidder response hook")); } diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java index 5532962afc2..d4eda0346ca 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; @@ -13,7 +13,7 @@ public class SampleItRejectingRawAuctionRequestHook implements RawAuctionRequest public Future> call( AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw auction request hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting raw auction request hook")); } @Override diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java index 0eeeee4375c..f2964a9871a 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationResultUtils; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; @@ -13,7 +13,7 @@ public class SampleItRejectingRawBidderResponseHook implements RawBidderResponse public Future> call( BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) { - return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw bidder response hook")); + return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting raw bidder response hook")); } @Override diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 5594cad0c65..104df273c3e 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -1164,6 +1164,13 @@ public void updateHooksMetricsShouldIncrementMetrics() { "module1", Stage.entrypoint, "hook1", ExecutionStatus.success, 5L, ExecutionAction.update); metrics.updateHooksMetrics( "module1", Stage.raw_auction_request, "hook2", ExecutionStatus.success, 5L, ExecutionAction.no_action); + metrics.updateHooksMetrics( + "module1", + Stage.raw_auction_request, + "hook2", + ExecutionStatus.success, + 5L, + ExecutionAction.no_invocation); metrics.updateHooksMetrics( "module1", Stage.processed_auction_request, @@ -1176,7 +1183,7 @@ public void updateHooksMetricsShouldIncrementMetrics() { metrics.updateHooksMetrics( "module2", Stage.raw_bidder_response, "hook2", ExecutionStatus.timeout, 7L, null); metrics.updateHooksMetrics( - "module2", Stage.processed_bidder_response, "hook3", ExecutionStatus.execution_failure, 5L, null); + "module2", Stage.all_processed_bid_responses, "hook3", ExecutionStatus.execution_failure, 5L, null); metrics.updateHooksMetrics( "module2", Stage.auction_response, "hook4", ExecutionStatus.invocation_failure, 5L, null); @@ -1194,6 +1201,9 @@ public void updateHooksMetricsShouldIncrementMetrics() { .isEqualTo(1); assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.noop").getCount()) .isEqualTo(1); + assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.no-invocation") + .getCount()) + .isEqualTo(1); assertThat(metricRegistry.timer("modules.module.module1.stage.rawauction.hook.hook2.duration").getCount()) .isEqualTo(1); @@ -1219,12 +1229,14 @@ public void updateHooksMetricsShouldIncrementMetrics() { assertThat(metricRegistry.timer("modules.module.module2.stage.rawbidresponse.hook.hook2.duration").getCount()) .isEqualTo(1); - assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.call").getCount()) + assertThat(metricRegistry.counter("modules.module.module2.stage.allprocbidresponses.hook.hook3.call") + .getCount()) .isEqualTo(1); - assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.execution-error") + assertThat(metricRegistry.counter("modules.module.module2.stage.allprocbidresponses.hook.hook3.execution-error") .getCount()) .isEqualTo(1); - assertThat(metricRegistry.timer("modules.module.module2.stage.procbidresponse.hook.hook3.duration").getCount()) + assertThat(metricRegistry.timer("modules.module.module2.stage.allprocbidresponses.hook.hook3.duration") + .getCount()) .isEqualTo(1); assertThat(metricRegistry.counter("modules.module.module2.stage.auctionresponse.hook.hook4.call").getCount()) @@ -1248,6 +1260,8 @@ public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed Account.empty("accountId"), "module2", ExecutionStatus.failure, null); metrics.updateAccountHooksMetrics( Account.empty("accountId"), "module3", ExecutionStatus.timeout, null); + metrics.updateAccountHooksMetrics( + Account.empty("accountId"), "module4", ExecutionStatus.success, ExecutionAction.no_invocation); // then assertThat(metricRegistry.counter("account.accountId.modules.module.module1.call").getCount()) @@ -1264,6 +1278,11 @@ public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed .isEqualTo(1); assertThat(metricRegistry.counter("account.accountId.modules.module.module3.failure").getCount()) .isEqualTo(1); + + assertThat(metricRegistry.counter("account.accountId.modules.module.module4.call").getCount()) + .isEqualTo(0); + assertThat(metricRegistry.counter("account.accountId.modules.module.module4.success.no-invocation").getCount()) + .isEqualTo(1); } @Test From d714f28afa375867c12286fcb634b3149a561c5f Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Wed, 13 Nov 2024 16:16:03 +0200 Subject: [PATCH 123/170] Module: Request correction (#3526) --- extra/bundle/pom.xml | 5 + extra/modules/pb-request-correction/pom.xml | 15 + .../pb-request-correction/src/lombok.config | 1 + .../core/RequestCorrectionProvider.java | 25 + .../correction/core/config/model/Config.java | 21 + .../core/correction/Correction.java | 9 + .../core/correction/CorrectionProducer.java | 11 + .../interstitial/InterstitialCorrection.java | 24 + .../InterstitialCorrectionProducer.java | 80 +++ .../useragent/UserAgentCorrection.java | 25 + .../UserAgentCorrectionProducer.java | 76 +++ .../correction/core/util/VersionUtil.java | 35 ++ .../RequestCorrectionModuleConfiguration.java | 38 ++ .../v1/RequestCorrectionModule.java | 32 ++ ...RequestCorrectionProcessedAuctionHook.java | 103 ++++ .../v1/model/AuctionRequestPayloadImpl.java | 13 + .../v1/model/InvocationResultImpl.java | 37 ++ .../core/RequestCorrectionProviderTest.java | 58 +++ .../InterstitialCorrectionProducerTest.java | 132 +++++ .../InterstitialCorrectionTest.java | 35 ++ .../UserAgentCorrectionProducerTest.java | 125 +++++ .../useragent/UserAgentCorrectionTest.java | 29 ++ .../correction/core/util/VersionUtilTest.java | 52 ++ ...estCorrectionProcessedAuctionHookTest.java | 120 +++++ extra/modules/pom.xml | 1 + .../server/functional/model/ModuleName.groovy | 3 +- .../config/ModuleHookImplementation.groovy | 3 +- .../config/PbRequestCorrectionConfig.groovy | 29 ++ .../model/config/PbsModulesConfig.groovy | 1 + .../model/request/auction/AppExt.groovy | 1 + .../model/request/auction/AppPrebid.groovy | 10 + .../model/request/auction/Imp.groovy | 2 +- .../tests/module/ModuleBaseSpec.groovy | 8 + .../PbRequestCorrectionSpec.groovy | 454 ++++++++++++++++++ .../server/functional/util/PBSUtils.groovy | 23 + 35 files changed, 1633 insertions(+), 3 deletions(-) create mode 100644 extra/modules/pb-request-correction/pom.xml create mode 100644 extra/modules/pb-request-correction/src/lombok.config create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java create mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java create mode 100644 extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index b6dfbac6ea8..74257f0b3cd 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -50,6 +50,11 @@ greenbids-real-time-data ${project.version} + + org.prebid.server.hooks.modules + pb-request-correction + ${project.version} + diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml new file mode 100644 index 00000000000..1686cadfaac --- /dev/null +++ b/extra/modules/pb-request-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.15.0-SNAPSHOT + + + pb-request-correction + + pb-request-correction + Request correction module + diff --git a/extra/modules/pb-request-correction/src/lombok.config b/extra/modules/pb-request-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-request-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java new file mode 100644 index 00000000000..3daa937c37c --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class RequestCorrectionProvider { + + private final List correctionProducers; + + public RequestCorrectionProvider(List correctionProducers) { + this.correctionProducers = Objects.requireNonNull(correctionProducers); + } + + public List corrections(Config config, BidRequest bidRequest) { + return correctionProducers.stream() + .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest)) + .map(correctionProducer -> correctionProducer.produce(config)) + .toList(); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java new file mode 100644 index 00000000000..44cac23337e --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class Config { + + boolean enabled; + + @JsonAlias("pbsdkAndroidInstlRemove") + @JsonProperty("pbsdk-android-instl-remove") + boolean interstitialCorrectionEnabled; + + @JsonAlias("pbsdkUaCleanup") + @JsonProperty("pbsdk-ua-cleanup") + boolean userAgentCorrectionEnabled; +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java new file mode 100644 index 00000000000..2cfda5fce68 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; + +public interface Correction { + + BidRequest apply(BidRequest bidRequest); +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..a92132656d5 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(Config config); +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java new file mode 100644 index 00000000000..75d86c511fb --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; + +public class InterstitialCorrection implements Correction { + + @Override + public BidRequest apply(BidRequest bidRequest) { + return bidRequest.toBuilder() + .imp(bidRequest.getImp().stream() + .map(InterstitialCorrection::removeInterstitial) + .toList()) + .build(); + } + + private static Imp removeInterstitial(Imp imp) { + final Integer interstitial = imp.getInstl(); + return interstitial != null && interstitial == 1 + ? imp.toBuilder().instl(null).build() + : imp; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java new file mode 100644 index 00000000000..c9bd1995867 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java @@ -0,0 +1,80 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import java.util.List; +import java.util.Optional; + +public class InterstitialCorrectionProducer implements CorrectionProducer { + + private static final InterstitialCorrection CORRECTION_INSTANCE = new InterstitialCorrection(); + + private static final String PREBID_MOBILE = "prebid-mobile"; + private static final String ANDROID = "android"; + + private static final int MAX_VERSION_MAJOR = 2; + private static final int MAX_VERSION_MINOR = 2; + private static final int MAX_VERSION_PATCH = 3; + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final App app = bidRequest.getApp(); + return config.isInterstitialCorrectionEnabled() + && hasInterstitialToRemove(bidRequest.getImp()) + && isPrebidMobile(app) + && isAndroid(app) + && isApplicableVersion(app); + } + + private static boolean hasInterstitialToRemove(List imps) { + for (Imp imp : imps) { + final Integer interstitial = imp.getInstl(); + if (interstitial != null && interstitial == 1) { + return true; + } + } + + return false; + } + + private static boolean isPrebidMobile(App app) { + final String source = Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getSource) + .orElse(null); + + return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE); + } + + private static boolean isAndroid(App app) { + return StringUtils.containsIgnoreCase(app.getBundle(), ANDROID); + } + + private static boolean isApplicableVersion(App app) { + return Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getVersion) + .map(InterstitialCorrectionProducer::checkVersion) + .orElse(false); + } + + private static boolean checkVersion(String version) { + return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH); + } + + @Override + public Correction produce(Config config) { + return CORRECTION_INSTANCE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java new file mode 100644 index 00000000000..f1b6b40eacc --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; + +import java.util.regex.Pattern; + +public class UserAgentCorrection implements Correction { + + private static final Pattern USER_AGENT_PATTERN = Pattern.compile("PrebidMobile/[0-9][^ ]*"); + + @Override + public BidRequest apply(BidRequest bidRequest) { + return bidRequest.toBuilder() + .device(correctDevice(bidRequest.getDevice())) + .build(); + } + + private static Device correctDevice(Device device) { + return device.toBuilder() + .ua(USER_AGENT_PATTERN.matcher(device.getUa()).replaceAll("")) + .build(); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java new file mode 100644 index 00000000000..f4c8d4f76dd --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UserAgentCorrectionProducer implements CorrectionProducer { + + private static final UserAgentCorrection CORRECTION_INSTANCE = new UserAgentCorrection(); + + private static final String PREBID_MOBILE = "prebid-mobile"; + private static final Pattern USER_AGENT_PATTERN = Pattern.compile(".*PrebidMobile/[0-9]+[^ ]*.*"); + + + private static final int MAX_VERSION_MAJOR = 2; + private static final int MAX_VERSION_MINOR = 1; + private static final int MAX_VERSION_PATCH = 6; + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final App app = bidRequest.getApp(); + return config.isUserAgentCorrectionEnabled() + && isPrebidMobile(app) + && isApplicableVersion(app) + && isApplicableDevice(bidRequest.getDevice()); + } + + private static boolean isPrebidMobile(App app) { + final String source = Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getSource) + .orElse(null); + + return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE); + } + + private static boolean isApplicableVersion(App app) { + return Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getVersion) + .map(UserAgentCorrectionProducer::checkVersion) + .orElse(false); + } + + private static boolean checkVersion(String version) { + return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH); + } + + private static boolean isApplicableDevice(Device device) { + return Optional.ofNullable(device) + .map(Device::getUa) + .filter(StringUtils::isNotEmpty) + .map(USER_AGENT_PATTERN::matcher) + .map(Matcher::matches) + .orElse(false); + } + + + @Override + public Correction produce(Config config) { + return CORRECTION_INSTANCE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java new file mode 100644 index 00000000000..2e84f01183e --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.util; + +public class VersionUtil { + + public static boolean isVersionLessThan(String versionAsString, int major, int minor, int patch) { + return compareVersion(versionAsString, major, minor, patch) < 0; + } + + private static int compareVersion(String versionAsString, int major, int minor, int patch) { + final String[] version = versionAsString.split("\\."); + + final int parsedMajor = getAtAsIntOrDefault(version, 0, -1); + final int parsedMinor = getAtAsIntOrDefault(version, 1, 0); + final int parsedPatch = getAtAsIntOrDefault(version, 2, 0); + + int diff = parsedMajor >= 0 ? parsedMajor - major : 1; + diff = diff == 0 ? parsedMinor - minor : diff; + diff = diff == 0 ? parsedPatch - patch : diff; + + return diff; + } + + private static int getAtAsIntOrDefault(String[] array, int index, int defaultValue) { + return array.length > index ? intOrDefault(array[index], defaultValue) : defaultValue; + } + + private static int intOrDefault(String intAsString, int defaultValue) { + try { + final int parsed = Integer.parseInt(intAsString); + return parsed >= 0 ? parsed : defaultValue; + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..ecbd725e42d --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.request.correction.spring.config; + +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial.InterstitialCorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent.UserAgentCorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.v1.RequestCorrectionModule; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "hooks." + RequestCorrectionModule.CODE, name = "enabled", havingValue = "true") +public class RequestCorrectionModuleConfiguration { + + @Bean + InterstitialCorrectionProducer interstitialCorrectionProducer() { + return new InterstitialCorrectionProducer(); + } + + @Bean + UserAgentCorrectionProducer userAgentCorrectionProducer() { + return new UserAgentCorrectionProducer(); + } + + @Bean + RequestCorrectionProvider requestCorrectionProvider(List correctionProducers) { + return new RequestCorrectionProvider(correctionProducers); + } + + @Bean + RequestCorrectionModule requestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider) { + return new RequestCorrectionModule(requestCorrectionProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java new file mode 100644 index 00000000000..10d20a3b823 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.Collections; + +public class RequestCorrectionModule implements Module { + + public static final String CODE = "pb-request-correction"; + + private final Collection> hooks; + + public RequestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java new file mode 100644 index 00000000000..50502e844db --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java @@ -0,0 +1,103 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.v1.model.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.pb.request.correction.v1.model.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; + +import java.util.List; +import java.util.Objects; + +public class RequestCorrectionProcessedAuctionHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "pb-request-correction-processed-auction-request"; + + private final RequestCorrectionProvider requestCorrectionProvider; + private final ObjectMapper mapper; + + public RequestCorrectionProcessedAuctionHook(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) { + this.requestCorrectionProvider = Objects.requireNonNull(requestCorrectionProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext context) { + + final Config config; + try { + config = moduleConfig(context.accountConfig()); + } catch (PreBidException e) { + return failure(e.getMessage()); + } + + if (config == null || !config.isEnabled()) { + return noAction(); + } + + final BidRequest bidRequest = payload.bidRequest(); + + final List corrections = requestCorrectionProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> + AuctionRequestPayloadImpl.of(applyCorrections(initialPayload.bidRequest(), corrections))) + .build(); + + return Future.succeededFuture(invocationResult); + } + + private Config moduleConfig(ObjectNode accountConfig) { + try { + return mapper.treeToValue(accountConfig, Config.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static BidRequest applyCorrections(BidRequest bidRequest, List corrections) { + BidRequest result = bidRequest; + for (Correction correction : corrections) { + result = correction.apply(result); + } + return result; + } + + private Future> failure(String message) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .message(message) + .action(InvocationAction.no_action) + .build()); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java new file mode 100644 index 00000000000..ca8bb6aa52d --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1.model; + +import com.iab.openrtb.request.BidRequest; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class AuctionRequestPayloadImpl implements AuctionRequestPayload { + + BidRequest bidRequest; +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java new file mode 100644 index 00000000000..96f90d14a29 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1.model; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Object moduleContext; + + Tags analyticsTags; +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java new file mode 100644 index 00000000000..56856d10c16 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core; + +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.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class RequestCorrectionProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private RequestCorrectionProvider target; + + @BeforeEach + public void setUp() { + target = new RequestCorrectionProvider(singletonList(correctionProducer)); + } + + @Test + public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(false); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).isEmpty(); + } + + @Test + public void correctionsShouldReturnProducedCorrection() { + // given + given(correctionProducer.shouldProduce(any(), any())).willReturn(true); + + final Correction correction = mock(Correction.class); + given(correctionProducer.produce(any())).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java new file mode 100644 index 00000000000..3a44b7158e3 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java @@ -0,0 +1,132 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InterstitialCorrectionProducerTest { + + private final InterstitialCorrectionProducer target = new InterstitialCorrectionProducer(); + + @Test + public void shouldProduceReturnsFalseIfCorrectionDisabled() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(false) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfThereIsNothingToDo() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(emptyList()) + .app(App.builder().build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfBundleNotAnAndroid() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundle") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", null), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfVersionInvalid() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundleAndroid") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsTrueWhenAllConditionsMatch() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundleAndroid") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isTrue(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java new file mode 100644 index 00000000000..490607a7d5e --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InterstitialCorrectionTest { + + private final InterstitialCorrection target = new InterstitialCorrection(); + + @Test + public void applyShouldCorrectInterstitial() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList( + Imp.builder().instl(0).build(), + Imp.builder().build(), + Imp.builder().instl(1).build())) + .build(); + + // when + final BidRequest result = target.apply(bidRequest); + + // then + assertThat(result) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getInstl) + .containsExactly(0, null, null); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java new file mode 100644 index 00000000000..cb7e3458bef --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java @@ -0,0 +1,125 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class UserAgentCorrectionProducerTest { + + private final UserAgentCorrectionProducer target = new UserAgentCorrectionProducer(); + + @Test + public void shouldProduceReturnsFalseIfCorrectionDisabled() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(false) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfThereIsNothingToDo() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfVersionInvalid() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfDeviceUserAgentDoesNotMatch() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("Blah blah").build()) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsTrueWhenAllConditionsMatch() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("Blah PrebidMobile/1asdf blah").build()) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isTrue(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java new file mode 100644 index 00000000000..c8ed5f6762d --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UserAgentCorrectionTest { + + private final UserAgentCorrection target = new UserAgentCorrection(); + + @Test + public void applyShouldCorrectUserAgent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("blah PrebidMobile/1asdf blah").build()) + .build(); + + // when + final BidRequest result = target.apply(bidRequest); + + // then + assertThat(result) + .extracting(BidRequest::getDevice) + .extracting(Device::getUa) + .isEqualTo("blah blah"); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java new file mode 100644 index 00000000000..8da1ec6a3c3 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java @@ -0,0 +1,52 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.util; + +import org.junit.jupiter.api.Test; + +import static java.lang.Integer.MAX_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +public class VersionUtilTest { + + @Test + public void isVersionLessThanShouldReturnFalseIfVersionGreaterThanRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 2, 3)).isFalse(); + } + + @Test + public void isVersionLessThenShouldReturnFalseIfVersionIsEqualToRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 4, 3)).isFalse(); + } + + @Test + public void isVersionLessThenShouldReturnTrueIfVersionIsLessThanRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 4, 3)).isTrue(); + } + + @Test + public void isVersionLessThenShouldReturnExpectedResults() { + // major + assertThat(VersionUtil.isVersionLessThan("0", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("3", 2, 2, 3)).isFalse(); + + // minor + assertThat(VersionUtil.isVersionLessThan("0." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.0", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.3", 2, 2, 3)).isFalse(); + + // patch + assertThat(VersionUtil.isVersionLessThan("0.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.1." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 2, 3)).isFalse(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java new file mode 100644 index 00000000000..9250e188cce --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java @@ -0,0 +1,120 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Future; +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.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class RequestCorrectionProcessedAuctionHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private RequestCorrectionProvider requestCorrectionProvider; + + private RequestCorrectionProcessedAuctionHook target; + + @Mock + private AuctionRequestPayload payload; + + @Mock + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder() + .enabled(true) + .interstitialCorrectionEnabled(true) + .build())); + + target = new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, MAPPER); + } + + @Test + public void callShouldReturnFailedResultOnInvalidConfiguration() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList()))); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type"); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionOnDisabledConfig() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder() + .enabled(false) + .interstitialCorrectionEnabled(true) + .build())); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() { + // given + given(requestCorrectionProvider.corrections(any(), any())).willReturn(emptyList()); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + }); + } + + @Test + public void callShouldReturnUpdate() { + // given + final Correction correction = mock(Correction.class); + given(requestCorrectionProvider.corrections(any(), any())).willReturn(singletonList(correction)); + + // when + final Future> result = target.call(payload, invocationContext); + + //then + assertThat(result.result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + }); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 5056c0c9914..d40b7e7829e 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -23,6 +23,7 @@ fiftyone-devicedetection pb-response-correction greenbids-real-time-data + pb-request-correction diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy index 5efcdf40709..2bc06ab7144 100644 --- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy @@ -6,7 +6,8 @@ enum ModuleName { PB_RICHMEDIA_FILTER("pb-richmedia-filter"), PB_RESPONSE_CORRECTION ("pb-response-correction"), - ORTB2_BLOCKING("ortb2-blocking") + ORTB2_BLOCKING("ortb2-blocking"), + PB_REQUEST_CORRECTION('pb-request-correction'), @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy index b5c57122a3f..247bdea4353 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -9,7 +9,8 @@ enum ModuleHookImplementation { PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"), RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses"), ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"), - ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response") + ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response"), + PB_REQUEST_CORRECTION_PROCESSED_AUCTION_REQUEST("pb-request-correction-processed-auction-request"), @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy new file mode 100644 index 00000000000..5d7a980115b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy @@ -0,0 +1,29 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class PbRequestCorrectionConfig { + + @JsonProperty("pbsdkAndroidInstlRemove") + Boolean interstitialCorrectionEnabled + @JsonProperty("pbsdkUaCleanup") + Boolean userAgentCorrectionEnabled + @JsonProperty("pbsdk-android-instl-remove") + Boolean interstitialCorrectionEnabledKebabCase + @JsonProperty("pbsdk-ua-cleanup") + Boolean userAgentCorrectionEnabledKebabCase + + Boolean enabled + + static PbRequestCorrectionConfig getDefaultConfigWithInterstitial(Boolean interstitialCorrectionEnabled = true, + Boolean enabled = true) { + new PbRequestCorrectionConfig(enabled: enabled, interstitialCorrectionEnabled: interstitialCorrectionEnabled) + } + + static PbRequestCorrectionConfig getDefaultConfigWithUserAgentCorrection(Boolean userAgentCorrectionEnabled = true, + Boolean enabled = true) { + new PbRequestCorrectionConfig(enabled: enabled, userAgentCorrectionEnabled: userAgentCorrectionEnabled) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy index f9121ae0b3a..59f640f966c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy @@ -12,4 +12,5 @@ class PbsModulesConfig { RichmediaFilter pbRichmediaFilter Ortb2BlockingConfig ortb2Blocking PbResponseCorrection pbResponseCorrection + PbRequestCorrectionConfig pbRequestCorrection } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy index b31926c14b5..ee3c1c9a8f0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy @@ -6,4 +6,5 @@ import groovy.transform.ToString class AppExt { AppExtData data + AppPrebid prebid } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy new file mode 100644 index 00000000000..edb365d4d6f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AppPrebid { + + String source + String version +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index dbea9b32624..13c97a36ba4 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -30,7 +30,7 @@ class Imp { Pmp pmp String displayManager String displayManagerVer - Integer instl + OperationState instl String tagId BigDecimal bidFloor Currency bidFloorCur diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy index 9cd310be252..c9bd259d3a9 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy @@ -2,13 +2,16 @@ package org.prebid.server.functional.tests.module import org.prebid.server.functional.model.config.Endpoint import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Stage import org.prebid.server.functional.tests.BaseSpec import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.ModuleName.PB_REQUEST_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST class ModuleBaseSpec extends BaseSpec { @@ -51,4 +54,9 @@ class ModuleBaseSpec extends BaseSpec { protected static Map getOrtb2BlockingSettings(boolean isEnabled = true) { ["hooks.${ORTB2_BLOCKING.code}.enabled": isEnabled as String] } + + protected static Map getRequestCorrectionSettings(Endpoint endpoint = OPENRTB2_AUCTION, Stage stage = PROCESSED_AUCTION_REQUEST) { + ["hooks.${PB_REQUEST_CORRECTION.code}.enabled": "true", + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_REQUEST_CORRECTION, [stage]))] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy new file mode 100644 index 00000000000..68b00bdd0d1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy @@ -0,0 +1,454 @@ +package org.prebid.server.functional.tests.module.pbrequestcorrection + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.PbRequestCorrectionConfig +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.AppExt +import org.prebid.server.functional.model.request.auction.AppPrebid +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.OperationState +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.OperationState.YES + +class PbRequestCorrectionSpec extends ModuleBaseSpec { + + private static final String PREBID_MOBILE = "prebid-mobile" + private static final String DEVICE_PREBID_MOBILE_PATTERN = "PrebidMobile/" + private static final String ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.1.5") + private static final String ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.2.3") + private static final String ANDROID = "android" + private static final String IOS = "IOS" + + private PrebidServerService pbsServiceWithRequestCorrectionModule = pbsServiceFactory.getService(requestCorrectionSettings) + + def "PBS should remove positive instl from imps for android app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp = imps + app.bundle = PBSUtils.getRandomCase(bundle) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl.every { it == null } + + where: + imps | bundle | requestCorrectionConfig + [Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + } + + def "PBS shouldn't remove negative instl from imps for android app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp = imps + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + imps << [[Imp.defaultImpression.tap { instl = OperationState.NO }], + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = OperationState.NO }], + [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = null }], + [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = OperationState.NO }]] + } + + def "PBS shouldn't remove positive instl from imps for not android or not prebid-mobile app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(source), version: PBSUtils.getRandomVersion(ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(bundle) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + bundle | source + IOS | PREBID_MOBILE + PBSUtils.randomString | PREBID_MOBILE + ANDROID | PBSUtils.randomString + ANDROID | PBSUtils.randomString + PREBID_MOBILE + ANDROID | PREBID_MOBILE + PBSUtils.randomString + } + + def "PBS shouldn't remove positive instl from imps for app when request correction is enabled for account but some required parameter is empty"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: source, version: version) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = instl + app.bundle = bundle + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + bundle | source | version | instl + null | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES + ANDROID | null | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES + ANDROID | PREBID_MOBILE | null | YES + ANDROID | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | null + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is threshold"() { + given: "Android APP bid request with version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.2.3") + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is higher then threshold"() { + given: "Android APP bid request with version higher then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.2.4")) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is disabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithInterstitial(interstitialCorrectionEnabled, enabled) + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + enabled | interstitialCorrectionEnabled + false | true + null | true + true | false + true | null + null | null + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is not applied for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig())) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS should remove pattern device.ua when request correction is enabled for account and user agent correction enabled"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.device.ua + + where: + deviceUa | requestCorrectionConfig + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true) + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true) + } + + def "PBS should remove only pattern device.ua when request correction is enabled for account and user agent correction enabled"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua.contains(deviceUa.replaceAll("PrebidMobile/[0-9][^ ]*", '').trim()) + + where: + deviceUa << ["${PBSUtils.randomNumber} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}", + "${PBSUtils.randomString} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString} ${PBSUtils.randomString}", + "${DEVICE_PREBID_MOBILE_PATTERN}", + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}", + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}" + ] + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and user agent correction disabled"() { + given: "Android APP bid request with version lover then version threshold" + def deviceUserAgent = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUserAgent) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithUserAgentCorrection(userAgentCorrectionEnabled, enabled) + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == deviceUserAgent + + where: + enabled | userAgentCorrectionEnabled + false | true + null | true + true | false + true | null + null | null + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and source not a prebid-mobile"() { + given: "Android APP bid request with version lover then version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: source, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + + where: + source << ["prebid", + "mobile", + PREBID_MOBILE + PBSUtils.randomString, + PBSUtils.randomString + PREBID_MOBILE, + "mobile-prebid", + PBSUtils.randomString] + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version biggest that threshold"() { + given: "Android APP bid request with version higher then version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6"))) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version threshold"() { + given: "Android APP bid request with version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.1.6")) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove device.ua pattern when request correction is enabled for account and version threshold"() { + given: "Android APP bid request with version higher then version threshold" + def randomDeviceUa = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6"))) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove device.ua pattern from device for android app when request correction is not applied for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def deviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig())) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain request device ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == deviceUa + } + + private static Account createAccountWithRequestCorrectionConfig(BidRequest bidRequest, + PbRequestCorrectionConfig requestCorrectionConfig) { + def pbsModulesConfig = new PbsModulesConfig(pbRequestCorrection: requestCorrectionConfig) + def accountHooksConfig = new AccountHooksConfiguration(modules: pbsModulesConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: bidRequest.accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index 2db485448b4..9ac2c148b5b 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -128,4 +128,27 @@ class PBSUtils implements ObjectMapperWrapper { throw new IllegalArgumentException("Unknown case type: $caseType") } } + + static String getRandomVersion(String minVersion = "0.0.0", String maxVersion = "99.99.99") { + def minParts = minVersion.split('\\.').collect { it.toInteger() } + def maxParts = maxVersion.split('\\.').collect { it.toInteger() } + def versionParts = [] + + def major = getRandomNumber(minParts[0], maxParts[0]) + versionParts << major + + def minorMin = (major == minParts[0]) ? minParts[1] : 0 + def minorMax = (major == maxParts[0]) ? maxParts[1] : 99 + def minor = getRandomNumber(minorMin, minorMax) + versionParts << minor + + if (minParts.size() > 2 || maxParts.size() > 2) { + def patchMin = (major == minParts[0] && minor == minParts[1]) ? minParts[2] : 0 + def patchMax = (major == maxParts[0] && minor == maxParts[1]) ? maxParts[2] : 99 + def patch = getRandomNumber(patchMin, patchMax) + versionParts << patch + } + def version = versionParts.join('.') + return (version >= minVersion && version <= maxVersion) ? version : getRandomVersion(minVersion, maxVersion) + } } From 097069966ce06d7ca75a29cdd8f9cccfc4c0da82 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:39:15 +0100 Subject: [PATCH 124/170] Core: Sample HttpBidderRequester Logs (#3546) --- .../prebid/server/bidder/HttpBidderRequester.java | 13 +++++++++---- .../server/spring/config/ServiceConfiguration.java | 6 ++++-- .../server/bidder/HttpBidderRequesterTest.java | 5 +++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 2a2fde46430..aa917b017cb 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -26,6 +26,7 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -62,24 +63,28 @@ public class HttpBidderRequester { private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private final HttpClient httpClient; private final BidderRequestCompletionTrackerFactory completionTrackerFactory; private final BidderErrorNotifier bidderErrorNotifier; private final HttpBidderRequestEnricher requestEnricher; private final JacksonMapper mapper; + private final double logSamplingRate; public HttpBidderRequester(HttpClient httpClient, BidderRequestCompletionTrackerFactory completionTrackerFactory, BidderErrorNotifier bidderErrorNotifier, HttpBidderRequestEnricher requestEnricher, - JacksonMapper mapper) { + JacksonMapper mapper, + double logSamplingRate) { this.httpClient = Objects.requireNonNull(httpClient); this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory); this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier); this.requestEnricher = Objects.requireNonNull(requestEnricher); this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; } /** @@ -241,9 +246,9 @@ private static byte[] gzip(byte[] value) { /** * Produces {@link Future} with {@link BidderCall} containing request and error description. */ - private static Future> failResponse(Throwable exception, HttpRequest httpRequest) { - logger.warn("Error occurred while sending HTTP request to a bidder url: {} with message: {}", - httpRequest.getUri(), exception.getMessage()); + private Future> failResponse(Throwable exception, HttpRequest httpRequest) { + conditionalLogger.warn("Error occurred while sending HTTP request to a bidder url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); logger.debug("Error occurred while sending HTTP request to a bidder url: {}", exception, httpRequest.getUri()); diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 71e014fcc23..7d657bc7b8c 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -752,11 +752,13 @@ HttpBidderRequester httpBidderRequester( HttpBidderRequestEnricher requestEnricher, JacksonMapper mapper) { - return new HttpBidderRequester(httpClient, + return new HttpBidderRequester( + httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier, requestEnricher, - mapper); + mapper, + logSamplingRate); } @Bean diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java index 83079b4d825..2970e643595 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java @@ -117,7 +117,7 @@ public void setUp() { expiredTimeout = timeoutFactory.create(clock.instant().minusMillis(1500L).toEpochMilli(), 1000L); target = new HttpBidderRequester( - httpClient, null, bidderErrorNotifier, requestEnricher, jacksonMapper); + httpClient, null, bidderErrorNotifier, requestEnricher, jacksonMapper, 0.0); given(bidder.makeBidderResponse(any(BidderCall.class), any(BidRequest.class))).willCallRealMethod(); } @@ -506,7 +506,8 @@ public void processBids(List bids) { }, bidderErrorNotifier, requestEnricher, - jacksonMapper); + jacksonMapper, + 0.0); final BidRequest bidRequest = bidRequestWithDeals("deal1", "deal2"); final BidderRequest bidderRequest = BidderRequest.builder() From af13ec80b0f88e5d7f5130fda3bb589ff1a82405 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:39:39 +0100 Subject: [PATCH 125/170] Core: Bid Adjustments Feature (#3542) --- docs/application-settings.md | 7 +- .../prebid/server/auction/BidsAdjuster.java | 140 +-- .../server/auction/model/AuctionContext.java | 12 + .../requestfactory/AuctionRequestFactory.java | 8 +- .../BidAdjustmentRulesValidator.java | 100 ++ .../BidAdjustmentsProcessor.java | 204 ++++ .../BidAdjustmentsResolver.java | 106 +++ .../BidAdjustmentsRetriever.java | 86 ++ .../model/BidAdjustmentType.java | 19 + .../bidadjustments/model/BidAdjustments.java | 52 + .../ext/request/ExtRequestBidAdjustments.java | 15 + .../request/ExtRequestBidAdjustmentsRule.java | 24 + .../openrtb/ext/request/ExtRequestPrebid.java | 5 + .../openrtb/ext/request/ImpMediaType.java | 3 + .../settings/model/AccountAuctionConfig.java | 4 + .../spring/config/ServiceConfiguration.java | 44 +- .../model/config/AccountAuctionConfig.groovy | 3 + .../request/auction/AdjustmentRule.groovy | 17 + .../request/auction/AdjustmentType.groovy | 13 + .../request/auction/BidAdjustment.groovy | 20 + .../auction/BidAdjustmentFactors.groovy | 1 - .../auction/BidAdjustmentMediaType.groovy | 5 +- .../request/auction/BidAdjustmentRule.groovy | 16 + .../model/request/auction/BidRequest.groovy | 5 + .../model/request/auction/Prebid.groovy | 1 + .../functional/tests/BidAdjustmentSpec.groovy | 886 +++++++++++++++++- .../server/auction/BidsAdjusterTest.java | 756 +-------------- .../AuctionRequestFactoryTest.java | 37 +- .../BidAdjustmentRulesValidatorTest.java | 306 ++++++ .../BidAdjustmentsProcessorTest.java | 823 ++++++++++++++++ .../BidAdjustmentsResolverTest.java | 243 +++++ .../BidAdjustmentsRetrieverTest.java | 396 ++++++++ .../model/BidAdjustmentsTest.java | 65 ++ 33 files changed, 3560 insertions(+), 862 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy create mode 100644 src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java create mode 100644 src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java create mode 100644 src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java create mode 100644 src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java create mode 100644 src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java diff --git a/docs/application-settings.md b/docs/application-settings.md index bf0dc61bfd0..7f164caa0dc 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -19,8 +19,13 @@ There are two ways to configure application settings: database and file. This do operational warning. - "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject the bid and log an operational warning. +- `auction.bidadjustments` - configuration JSON for default bid adjustments +- `auction.bidadjustments.mediatype.{banner, video-instream, video-outstream, audio, native, *}.{, *}.{, *}[]` - array of bid adjustment to be applied to any bid of the provided mediatype, and (`*` means ANY) +- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static) +- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment +- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment - `auction.events.enabled` - enables events for account if true -- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true. +- `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true. - `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false. - `auction.price-floors.fetch.url` - url to fetch price floors data from. - `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000. diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java index 8134662fcd9..4ae7a6e3e3e 100644 --- a/src/main/java/org/prebid/server/auction/BidsAdjuster.java +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -1,32 +1,19 @@ package org.prebid.server.auction; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.DecimalNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.Bid; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.PriceFloorEnforcer; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.util.ObjectUtil; -import org.prebid.server.util.PbsUtil; import org.prebid.server.validation.ResponseBidValidator; import org.prebid.server.validation.model.ValidationResult; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -35,29 +22,20 @@ public class BidsAdjuster { - private static final String ORIGINAL_BID_CPM = "origbidcpm"; - private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; - private final ResponseBidValidator responseBidValidator; - private final CurrencyConversionService currencyService; - private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; private final PriceFloorEnforcer priceFloorEnforcer; + private final BidAdjustmentsProcessor bidAdjustmentsProcessor; private final DsaEnforcer dsaEnforcer; - private final JacksonMapper mapper; public BidsAdjuster(ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyService, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, PriceFloorEnforcer priceFloorEnforcer, - DsaEnforcer dsaEnforcer, - JacksonMapper mapper) { + BidAdjustmentsProcessor bidAdjustmentsProcessor, + DsaEnforcer dsaEnforcer) { this.responseBidValidator = Objects.requireNonNull(responseBidValidator); - this.currencyService = Objects.requireNonNull(currencyService); - this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); + this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor); this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); - this.mapper = Objects.requireNonNull(mapper); } public List validateAndAdjustBids(List auctionParticipations, @@ -66,12 +44,18 @@ public List validateAndAdjustBids(List validBidderResponse(auctionParticipation, auctionContext, aliases)) - .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) + + .map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids( + auctionParticipation, + auctionContext.getBidRequest(), + auctionContext.getBidAdjustments())) + .map(auctionParticipation -> priceFloorEnforcer.enforce( auctionContext.getBidRequest(), auctionParticipation, auctionContext.getAccount(), auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .map(auctionParticipation -> dsaEnforcer.enforce( auctionContext.getBidRequest(), auctionParticipation, @@ -137,104 +121,4 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); } - - private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, - BidRequest bidRequest) { - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - - final List bidderBids = seatBid.getBids(); - if (bidderBids.isEmpty()) { - return auctionParticipation; - } - - final List updatedBidderBids = new ArrayList<>(bidderBids.size()); - final List errors = new ArrayList<>(seatBid.getErrors()); - final String adServerCurrency = bidRequest.getCur().getFirst(); - - for (final BidderBid bidderBid : bidderBids) { - try { - final BidderBid updatedBidderBid = - updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); - updatedBidderBids.add(updatedBidderBid); - } catch (PreBidException e) { - errors.add(BidderError.generic(e.getMessage())); - } - } - - final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() - .bids(updatedBidderBids) - .errors(errors) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, - BidderResponse bidderResponse, - BidRequest bidRequest, - String adServerCurrency) { - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidderBid.getBidCurrency(); - final BigDecimal price = bid.getPrice(); - - final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); - - final BigDecimal priceAdjustmentFactor = - bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); - final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); - - final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); - - updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); - - final Bid.BidBuilder bidBuilder = bid.toBuilder(); - if (adjustedPrice.compareTo(price) != 0) { - bidBuilder.price(adjustedPrice); - } - - if (!updatedBidExt.isEmpty()) { - bidBuilder.ext(updatedBidExt); - } - - return bidderBid.toBuilder().bid(bidBuilder.build()).build(); - } - - private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { - final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); - - return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); - } - - private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); - return prebid != null ? prebid.getBidadjustmentfactors() : null; - } - - private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { - return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 - ? price.multiply(priceAdjustmentFactor) - : price; - } - - private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); - if (StringUtils.isNotBlank(bidCurrency)) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); - } - } - - private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { - node.set(propertyName, propertyValue); - } } diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 3ee60aab4fa..5dbe83c3ff2 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -8,6 +8,7 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.geolocation.model.GeoInfo; @@ -17,6 +18,7 @@ import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -71,6 +73,10 @@ public class AuctionContext { CachedDebugLog cachedDebugLog; + @JsonIgnore + @Builder.Default + BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap()); + public AuctionContext with(Account account) { return this.toBuilder().account(account).build(); } @@ -124,6 +130,12 @@ public AuctionContext with(GeoInfo geoInfo) { .build(); } + public AuctionContext with(BidAdjustments bidAdjustments) { + return this.toBuilder() + .bidAdjustments(bidAdjustments) + .build(); + } + public AuctionContext withRequestRejected() { return this.toBuilder() .requestRejected(true) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index bd720a25f2b..34140a26228 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -17,6 +17,7 @@ import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; @@ -50,6 +51,7 @@ public class AuctionRequestFactory { private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; private final GeoLocationServiceWrapper geoLocationServiceWrapper; + private final BidAdjustmentsRetriever bidAdjustmentsRetriever; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); @@ -66,7 +68,8 @@ public AuctionRequestFactory(long maxRequestSize, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, JacksonMapper mapper, - GeoLocationServiceWrapper geoLocationServiceWrapper) { + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsRetriever bidAdjustmentsRetriever) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); @@ -82,6 +85,7 @@ public AuctionRequestFactory(long maxRequestSize, this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); + this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever); } /** @@ -142,6 +146,8 @@ public Future enrichAuctionContext(AuctionContext initialContext .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) .map(auctionContext::with)) + .map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext))) + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java new file mode 100644 index 00000000000..9ddeefb6e2e --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java @@ -0,0 +1,100 @@ +package org.prebid.server.bidadjustments; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class BidAdjustmentRulesValidator { + + public static final Set SUPPORTED_MEDIA_TYPES = Set.of( + BidAdjustmentsResolver.WILDCARD, + ImpMediaType.banner.toString(), + ImpMediaType.audio.toString(), + ImpMediaType.video_instream.toString(), + ImpMediaType.video_outstream.toString(), + ImpMediaType.xNative.toString()); + + private BidAdjustmentRulesValidator() { + + } + + public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException { + if (bidAdjustments == null) { + return; + } + + final Map>>> mediatypes = + bidAdjustments.getMediatype(); + + if (MapUtils.isEmpty(mediatypes)) { + return; + } + + for (String mediatype : mediatypes.keySet()) { + if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + if (MapUtils.isEmpty(bidders)) { + throw new ValidationException("no bidders found in %s".formatted(mediatype)); + } + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + + if (MapUtils.isEmpty(deals)) { + throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder)); + } + + for (String dealId : deals.keySet()) { + final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId); + validateRules(deals.get(dealId), path); + } + } + } + } + } + + private static void validateRules(List rules, + String path) throws ValidationException { + + if (rules == null) { + throw new ValidationException("no bid adjustment rules found in %s".formatted(path)); + } + + for (ExtRequestBidAdjustmentsRule rule : rules) { + final BidAdjustmentType type = rule.getAdjType(); + final String currency = rule.getCurrency(); + final BigDecimal value = rule.getValue(); + + final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency); + + final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN; + + final boolean invalidCpm = type == BidAdjustmentType.CPM + && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE)); + + final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER + && isValueNotInRange(value, 0, 100); + + final boolean invalidStatic = type == BidAdjustmentType.STATIC + && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE)); + + if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) { + throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path)); + } + } + } + + private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) { + return value == null + || value.compareTo(BigDecimal.valueOf(minValue)) < 0 + || value.compareTo(BigDecimal.valueOf(maxValue)) >= 0; + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java new file mode 100644 index 00000000000..8bd10535b98 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -0,0 +1,204 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.ImpMediaTypeResolver; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.PbsUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class BidAdjustmentsProcessor { + + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; + + private final CurrencyConversionService currencyService; + private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final BidAdjustmentsResolver bidAdjustmentsResolver; + private final JacksonMapper mapper; + + public BidAdjustmentsProcessor(CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidAdjustmentsResolver bidAdjustmentsResolver, + JacksonMapper mapper) { + + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.bidAdjustmentsResolver = Objects.requireNonNull(bidAdjustmentsResolver); + this.mapper = Objects.requireNonNull(mapper); + } + + public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionParticipation, + BidRequest bidRequest, + BidAdjustments bidAdjustments) { + + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + + final List bidderBids = seatBid.getBids(); + if (bidderBids.isEmpty()) { + return auctionParticipation; + } + + final List errors = new ArrayList<>(seatBid.getErrors()); + final String bidder = auctionParticipation.getBidder(); + + final List updatedBidderBids = bidderBids.stream() + .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, bidAdjustments, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + final BidderResponse updatedBidderResponse = bidderResponse.with(seatBid.toBuilder() + .bids(updatedBidderBids) + .errors(errors) + .build()); + + return auctionParticipation.with(updatedBidderResponse); + } + + private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); + return prebid != null ? prebid.getBidadjustmentfactors() : null; + } + + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { + return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 + ? price.multiply(priceAdjustmentFactor) + : price; + } + + private BidderBid applyBidAdjustments(BidderBid bidderBid, + BidRequest bidRequest, + String bidder, + BidAdjustments bidAdjustments, + List errors) { + try { + final Price originalPrice = getOriginalPrice(bidderBid); + final Price priceWithFactorsApplied = applyBidAdjustmentFactors( + originalPrice, + bidderBid, + bidder, + bidRequest); + final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules( + priceWithFactorsApplied, + bidderBid, + bidder, + bidRequest, + bidAdjustments); + return updateBid(originalPrice, priceWithAdjustmentsApplied, bidderBid, bidRequest); + } catch (PreBidException e) { + errors.add(BidderError.generic(e.getMessage())); + return null; + } + } + + private BidderBid updateBid(Price originalPrice, Price adjustedPrice, BidderBid bidderBid, BidRequest bidRequest) { + final Bid bid = bidderBid.getBid(); + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + final BigDecimal originalBidPrice = originalPrice.getValue(); + final String originalBidCurrency = originalPrice.getCurrency(); + updatedBidExt.set(ORIGINAL_BID_CPM, new DecimalNode(originalBidPrice)); + if (StringUtils.isNotBlank(originalBidCurrency)) { + updatedBidExt.set(ORIGINAL_BID_CURRENCY, new TextNode(originalBidCurrency)); + } + + final String requestCurrency = bidRequest.getCur().getFirst(); + final BigDecimal requestCurrencyPrice = currencyService.convertCurrency( + adjustedPrice.getValue(), + bidRequest, + adjustedPrice.getCurrency(), + requestCurrency); + + return bidderBid.toBuilder() + .bidCurrency(requestCurrency) + .bid(bid.toBuilder() + .ext(updatedBidExt) + .price(requestCurrencyPrice) + .build()) + .build(); + } + + private Price getOriginalPrice(BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + return Price.of(StringUtils.stripToNull(bidCurrency), price); + } + + private Price applyBidAdjustmentFactors(Price bidPrice, BidderBid bidderBid, String bidder, BidRequest bidRequest) { + final String bidCurrency = bidPrice.getCurrency(); + final BigDecimal price = bidPrice.getValue(); + + final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidder, bidRequest, bidderBid); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, price); + + return Price.of(bidCurrency, adjustedPrice.compareTo(price) != 0 ? adjustedPrice : price); + } + + private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { + final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); + if (adjustmentFactors == null) { + return null; + } + + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), + bidRequest.getImp(), + bidderBid.getType()); + + return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); + } + + private Price applyBidAdjustmentRules(Price bidPrice, + BidderBid bidderBid, + String bidder, + BidRequest bidRequest, + BidAdjustments bidAdjustments) { + + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidPrice.getCurrency(); + final BigDecimal price = bidPrice.getValue(); + + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bid.getImpid(), + bidRequest.getImp(), + bidderBid.getType()); + + return bidAdjustmentsResolver.resolve( + Price.of(bidCurrency, price), + bidRequest, + bidAdjustments, + mediaType == null || mediaType == ImpMediaType.video ? ImpMediaType.video_instream : mediaType, + bidder, + bid.getDealid()); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java new file mode 100644 index 00000000000..ffac1cbc51a --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java @@ -0,0 +1,106 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; +import org.prebid.server.util.dsl.config.PrebidConfigParameter; +import org.prebid.server.util.dsl.config.PrebidConfigParameters; +import org.prebid.server.util.dsl.config.PrebidConfigSource; +import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; +import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; +import org.prebid.server.util.dsl.config.impl.SimpleParameters; +import org.prebid.server.util.dsl.config.impl.SimpleSource; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class BidAdjustmentsResolver { + + public static final String WILDCARD = "*"; + public static final String DELIMITER = "|"; + + private final PrebidConfigMatchingStrategy matchingStrategy; + private final CurrencyConversionService currencyService; + + public BidAdjustmentsResolver(CurrencyConversionService currencyService) { + this.currencyService = Objects.requireNonNull(currencyService); + this.matchingStrategy = new MostAccurateCombinationStrategy(); + } + + public Price resolve(Price initialPrice, + BidRequest bidRequest, + BidAdjustments bidAdjustments, + ImpMediaType targetMediaType, + String targetBidder, + String targetDealId) { + + final List adjustmentsRules = findRules( + bidAdjustments, + targetMediaType, + targetBidder, + targetDealId); + + return adjustPrice(initialPrice, adjustmentsRules, bidRequest); + } + + private List findRules(BidAdjustments bidAdjustments, + ImpMediaType targetMediaType, + String targetBidder, + String targetDealId) { + + final Map> rules = bidAdjustments.getRules(); + final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); + final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId); + + final String rule = matchingStrategy.match(source, parameters); + return rule == null ? Collections.emptyList() : rules.get(rule); + } + + private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) { + final List conditionsMatchers = List.of( + SimpleDirectParameter.of(mediaType.toString()), + SimpleDirectParameter.of(bidder), + StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard()); + + return SimpleParameters.of(conditionsMatchers); + } + + private Price adjustPrice(Price price, + List bidAdjustmentRules, + BidRequest bidRequest) { + + String resolvedCurrency = price.getCurrency(); + BigDecimal resolvedPrice = price.getValue(); + + for (ExtRequestBidAdjustmentsRule rule : bidAdjustmentRules) { + final BidAdjustmentType adjustmentType = rule.getAdjType(); + final BigDecimal adjustmentValue = rule.getValue(); + final String adjustmentCurrency = rule.getCurrency(); + + switch (adjustmentType) { + case MULTIPLIER -> resolvedPrice = BidderUtil.roundFloor(resolvedPrice.multiply(adjustmentValue)); + case CPM -> { + final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency( + adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency); + resolvedPrice = BidderUtil.roundFloor(resolvedPrice.subtract(convertedAdjustmentValue)); + } + case STATIC -> { + resolvedPrice = adjustmentValue; + resolvedCurrency = adjustmentCurrency; + } + } + } + + return Price.of(resolvedCurrency, resolvedPrice); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java new file mode 100644 index 00000000000..6a151754bb2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java @@ -0,0 +1,86 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.validation.ValidationException; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsRetriever { + + private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsRetriever.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + private final double samplingRate; + + public BidAdjustmentsRetriever(JacksonMapper mapper, + JsonMerger jsonMerger, + double samplingRate) { + this.mapper = Objects.requireNonNull(mapper).mapper(); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.samplingRate = samplingRate; + } + + public BidAdjustments retrieve(AuctionContext auctionContext) { + final List debugWarnings = auctionContext.getDebugWarnings(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + + final JsonNode requestBidAdjustmentsNode = Optional.ofNullable(auctionContext.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode accountBidAdjustmentsNode = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidAdjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode mergedBidAdjustmentsNode = jsonMerger.merge( + requestBidAdjustmentsNode, + accountBidAdjustmentsNode); + + final List resolvedWarnings = debugEnabled ? debugWarnings : null; + return convertAndValidate(mergedBidAdjustmentsNode, resolvedWarnings, "request") + .or(() -> convertAndValidate(accountBidAdjustmentsNode, resolvedWarnings, "account")) + .orElse(BidAdjustments.of(Collections.emptyMap())); + } + + private Optional convertAndValidate(JsonNode bidAdjustmentsNode, + List debugWarnings, + String errorLocation) { + try { + final ExtRequestBidAdjustments accountBidAdjustments = mapper.convertValue( + bidAdjustmentsNode, + ExtRequestBidAdjustments.class); + + BidAdjustmentRulesValidator.validate(accountBidAdjustments); + return Optional.of(BidAdjustments.of(accountBidAdjustments)); + } catch (IllegalArgumentException | ValidationException e) { + final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); + if (debugWarnings != null) { + debugWarnings.add(message); + } + conditionalLogger.error(message, samplingRate); + return Optional.empty(); + } + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java new file mode 100644 index 00000000000..e9b790e5eab --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidadjustments.model; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum BidAdjustmentType { + + CPM, MULTIPLIER, STATIC, UNKNOWN; + + @SuppressWarnings("unused") + @JsonCreator + public static BidAdjustmentType of(String name) { + try { + return BidAdjustmentType.valueOf(name.toUpperCase()); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } + +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java new file mode 100644 index 00000000000..385a7644811 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java @@ -0,0 +1,52 @@ +package org.prebid.server.bidadjustments.model; + +import lombok.Value; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; +import org.prebid.server.bidadjustments.BidAdjustmentsResolver; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class BidAdjustments { + + private static final String RULE_SCHEME = + "%s" + BidAdjustmentsResolver.DELIMITER + "%s" + BidAdjustmentsResolver.DELIMITER + "%s"; + + Map> rules; + + public static BidAdjustments of(ExtRequestBidAdjustments bidAdjustments) { + if (bidAdjustments == null) { + return BidAdjustments.of(Collections.emptyMap()); + } + + final Map> rules = new HashMap<>(); + + final Map>>> mediatypes = + bidAdjustments.getMediatype(); + + if (MapUtils.isEmpty(mediatypes)) { + return BidAdjustments.of(Collections.emptyMap()); + } + + for (String mediatype : mediatypes.keySet()) { + if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + for (String dealId : deals.keySet()) { + rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); + } + } + } + } + + return BidAdjustments.of(MapUtils.unmodifiableMap(rules)); + } + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java new file mode 100644 index 00000000000..ab0565ce44e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java @@ -0,0 +1,15 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Builder(toBuilder = true) +@Value +public class ExtRequestBidAdjustments { + + Map>>> mediatype; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java new file mode 100644 index 00000000000..a857575a85f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java @@ -0,0 +1,24 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; + +import java.math.BigDecimal; + +@Builder(toBuilder = true) +@Value +public class ExtRequestBidAdjustmentsRule { + + @JsonProperty("adjtype") + BidAdjustmentType adjType; + + BigDecimal value; + + String currency; + + public String toString() { + return "[adjtype=%s, value=%s, currency=%s]".formatted(adjType, value, currency); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index 6dee1b7ba38..cb325bd088a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -50,6 +50,11 @@ public class ExtRequestPrebid { */ ExtRequestBidAdjustmentFactors bidadjustmentfactors; + /** + * Defines the contract for bidrequest.ext.prebid.bidadjustments + */ + ObjectNode bidadjustments; + /** * Defines the contract for bidrequest.ext.prebid.currency */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java index 732ddca6236..d619ed27e80 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java @@ -9,6 +9,8 @@ public enum ImpMediaType { @JsonProperty("native") xNative, video, + @JsonProperty("video-instream") + video_instream, @JsonProperty("video-outstream") video_outstream; @@ -16,6 +18,7 @@ public enum ImpMediaType { public String toString() { return this == xNative ? "native" : this == video_outstream ? "video-outstream" + : this == video_instream ? "video-instream" : super.toString(); } } diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index 9708c23bb1c..9943535aa7e 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; import org.prebid.server.spring.config.bidder.model.MediaType; @@ -33,6 +34,9 @@ public class AccountAuctionConfig { @JsonAlias("bid-validations") AccountBidValidationConfig bidValidations; + @JsonProperty("bidadjustments") + ObjectNode bidAdjustments; + AccountEventsConfig events; @JsonAlias("price-floors") diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 7d657bc7b8c..c2ee2ca5dfa 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -66,6 +66,9 @@ import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; +import org.prebid.server.bidadjustments.BidAdjustmentsResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.BidderErrorNotifier; @@ -424,7 +427,8 @@ AuctionRequestFactory auctionRequestFactory( AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, JacksonMapper mapper, - GeoLocationServiceWrapper geoLocationServiceWrapper) { + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsRetriever bidAdjustmentsRetriever) { return new AuctionRequestFactory( maxRequestSize, @@ -440,7 +444,8 @@ AuctionRequestFactory auctionRequestFactory( auctionPrivacyContextFactory, debugResolver, mapper, - geoLocationServiceWrapper); + geoLocationServiceWrapper, + bidAdjustmentsRetriever); } @Bean @@ -883,19 +888,11 @@ ExchangeService exchangeService( @Bean BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyConversionService, PriceFloorEnforcer priceFloorEnforcer, DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, - JacksonMapper mapper) { + BidAdjustmentsProcessor bidAdjustmentsProcessor) { - return new BidsAdjuster( - responseBidValidator, - currencyConversionService, - bidAdjustmentFactorResolver, - priceFloorEnforcer, - dsaEnforcer, - mapper); + return new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); } @Bean @@ -1174,6 +1171,29 @@ SkippedAuctionService skipAuctionService(StoredResponseProcessor storedResponseP return new SkippedAuctionService(storedResponseProcessor, bidResponseCreator); } + @Bean + BidAdjustmentsRetriever bidAdjustmentsRetriever(JacksonMapper mapper, JsonMerger jsonMerger) { + return new BidAdjustmentsRetriever(mapper, jsonMerger, logSamplingRate); + } + + @Bean + BidAdjustmentsResolver bidAdjustmentsResolver(CurrencyConversionService currencyService) { + return new BidAdjustmentsResolver(currencyService); + } + + @Bean + BidAdjustmentsProcessor bidAdjustmentsProcessor(CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidAdjustmentsResolver bidAdjustmentsResolver, + JacksonMapper mapper) { + + return new BidAdjustmentsProcessor( + currencyService, + bidAdjustmentFactorResolver, + bidAdjustmentsResolver, + mapper); + } + private static List splitToList(String listAsString) { return splitToCollection(listAsString, ArrayList::new); } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 2983ac3f731..4e423c03312 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.request.auction.BidAdjustment import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.MediaType @@ -26,6 +27,8 @@ class AccountAuctionConfig { Map preferredMediaType @JsonProperty("privacysandbox") PrivacySandbox privacySandbox + @JsonProperty("bidadjustments") + BidAdjustment bidAdjustments @JsonProperty("price_granularity") PriceGranularityType priceGranularitySnakeCase diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy new file mode 100644 index 00000000000..953f66fd988 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.Currency + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class AdjustmentRule { + + @JsonProperty('adjtype') + AdjustmentType adjustmentType + BigDecimal value + Currency currency +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy new file mode 100644 index 00000000000..20574d525a1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum AdjustmentType { + + MULTIPLIER, CPM, STATIC, UNKNOWN + + @JsonValue + String getValue() { + name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy new file mode 100644 index 00000000000..7f7250a6a75 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class BidAdjustment { + + Map mediaType + Integer version + + static getDefaultWithSingleMediaTypeRule(BidAdjustmentMediaType type, + BidAdjustmentRule rule, + Integer version = PBSUtils.randomNumber) { + new BidAdjustment(mediaType: [(type): rule], version: version) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy index a005d407241..9cb90edb27b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy @@ -15,7 +15,6 @@ class BidAdjustmentFactors { Map adjustments Map> mediaTypes - @JsonAnyGetter Map getAdjustments() { adjustments diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy index a959f5b800c..26a58655215 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy @@ -8,7 +8,10 @@ enum BidAdjustmentMediaType { AUDIO("audio"), NATIVE("native"), VIDEO("video"), - VIDEO_OUTSTREAM("video-outstream") + VIDEO_IN_STREAM("video-instream"), + VIDEO_OUT_STREAM("video-outstream"), + ANY('*'), + UNKNOWN('unknown') @JsonValue String value diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy new file mode 100644 index 00000000000..4fcfc1125e1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class BidAdjustmentRule { + + @JsonProperty('*') + Map> wildcardBidder + Map> generic + Map> alias +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index f60cabcc606..aa9da45a4b6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -10,6 +10,7 @@ import static org.prebid.server.functional.model.request.auction.DistributionCha import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO @EqualsAndHashCode @@ -48,6 +49,10 @@ class BidRequest { getDefaultRequest(channel, Imp.getDefaultImpression(VIDEO)) } + static BidRequest getDefaultNativeRequest(DistributionChannel channel = SITE) { + getDefaultRequest(channel, Imp.getDefaultImpression(NATIVE)) + } + static BidRequest getDefaultAudioRequest(DistributionChannel channel = SITE) { getDefaultRequest(channel, Imp.getDefaultImpression(AUDIO)) } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy index d99122f180d..ba89f5680fa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy @@ -15,6 +15,7 @@ class Prebid { Map aliases Map aliasgvlids BidAdjustmentFactors bidAdjustmentFactors + BidAdjustment bidAdjustments PrebidCurrency currency Targeting targeting TraceLevel trace diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 20536435161..90c6c764929 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -1,22 +1,71 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse +import org.prebid.server.functional.model.request.auction.AdjustmentRule +import org.prebid.server.functional.model.request.auction.AdjustmentType +import org.prebid.server.functional.model.request.auction.BidAdjustment import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors +import org.prebid.server.functional.model.request.auction.BidAdjustmentRule import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.util.PBSUtils +import java.math.RoundingMode +import java.time.Instant + import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.RUBICON +import static org.prebid.server.functional.model.request.auction.AdjustmentType.CPM +import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER +import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_ARTICLE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer +import static org.prebid.server.functional.util.PBSUtils.getRandomDecimal class BidAdjustmentSpec extends BaseSpec { + private static final String WILDCARD = '*' + private static final BigDecimal MIN_ADJUST_VALUE = 0 + private static final BigDecimal MAX_MULTIPLIER_ADJUST_VALUE = 99 + private static final BigDecimal MAX_CPM_ADJUST_VALUE = Integer.MAX_VALUE + private static final BigDecimal MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE + private static final Currency DEFAULT_CURRENCY = USD + private static final int BID_ADJUST_PRECISION = 4 + private static final int PRICE_PRECISION = 3 + private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, + (GBP): 0.793776804452961], + (GBP): [(USD): 1.2597999770088517, + (EUR): 1.1495574203931487], + (EUR): [(USD): 1.3429368029739777]] + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { + setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) + } + private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig) + def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment" def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { @@ -28,10 +77,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price * + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * bidAdjustmentFactor where: @@ -40,7 +89,7 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should prefer bid price adjustment based on media type when request has per-media-type bid adjustment factors"() { given: "Default bid request with bid adjustment" - def bidAdjustment = PBSUtils.randomDecimal + def bidAdjustment = randomDecimal def mediaTypeBidAdjustment = bidAdjustmentFactor def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap { @@ -54,10 +103,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price * + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * mediaTypeBidAdjustment where: @@ -66,7 +115,7 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should adjust bid price for bidder only when request contains bid adjustment for corresponding bidder"() { given: "Default bid request with bid adjustment" - def bidAdjustment = PBSUtils.randomDecimal + def bidAdjustment = randomDecimal def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap { adjustments = [(adjustmentBidder): bidAdjustment] @@ -78,10 +127,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should not be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price where: adjustmentBidder << [RUBICON, APPNEXUS] @@ -102,10 +151,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should not be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price where: adjustmentMediaType << [VIDEO, NATIVE] @@ -125,7 +174,7 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS should fail the request" def exception = thrown(PrebidServerException) @@ -133,6 +182,819 @@ class BidAdjustmentSpec extends BaseSpec { assert exception.responseBody.contains("Invalid request format: request.ext.prebid.bidadjustmentfactors.$bidderName.value must be a positive number") where: - bidAdjustmentFactor << [0, PBSUtils.randomNegativeNumber] + bidAdjustmentFactor << [MIN_ADJUST_VALUE, PBSUtils.randomNegativeNumber] + } + + def "PBS should adjust bid price for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain default currency" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder with specific dealId when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def dealId = PBSUtils.randomString + def currency = USD + def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.imp.add(Imp.defaultImpression) + bidRequest.cur = [currency] + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + response.seatbid.first.bid.find { it.dealid == dealId } + assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder when account config has bidAdjustments"() { + given: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def currency = USD + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Account in the DB with bidAdjustments" + def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to request config" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should prioritize exact bid price adjustment for matching bidder when request has exact and general bidAdjustment"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: PBSUtils.randomPrice, currency: currency) + def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: PBSUtils.randomPrice, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType) + def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType) + assert response.seatbid.first.bid.first.price == adjustedBidPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + firstRuleType | secondRuleType + MULTIPLIER | CPM + MULTIPLIER | STATIC + MULTIPLIER | MULTIPLIER + CPM | CPM + CPM | STATIC + CPM | MULTIPLIER + STATIC | CPM + STATIC | STATIC + STATIC | MULTIPLIER + } + + def "PBS should convert CPM currency before adjustment when it different from original response currency"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.randomPrice, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [EUR] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def convertedAdjustment = convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) + def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) + assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, bidRequest.cur.first) + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == bidRequest.cur + } + + def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [EUR] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response with JPY currency" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted and converted to original request cur" + assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidRequest.cur.first) + assert response.cur == bidRequest.cur.first + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == bidRequest.cur + } + + def "PBS should apply bidAdjustments after bidAdjustmentFactors when both are present"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def bidAdjustmentFactorsPrice = PBSUtils.randomPrice + def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice + assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when request has invalid value bidAdjustments config"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + + CPM | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + CPM | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + CPM | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | MAX_CPM_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + + STATIC | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + STATIC | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + } + + def "PBS shouldn't adjust bid price for matching bidder when request has different bidder name in bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown mediatype"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentPrice = PBSUtils.randomPrice + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown adjustmentType"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " + + "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS shouldn't adjust bid price for matching bidder when multiplier bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + where: + adjustmentType << [CPM, STATIC] + } + + private static Map getExternalCurrencyConverterConfig() { + ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, + "currency-converter.external-rates.enabled" : "true", + "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), + "currency-converter.external-rates.default-timeout-ms": "4000", + "currency-converter.external-rates.refresh-period-ms" : "900000"] + } + + private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { + return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) + } + + private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { + def conversionRate + if (fromCurrency == toCurrency) { + conversionRate = 1 + } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { + conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] + } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { + conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + } else { + conversionRate = getCrossConversionRate(fromCurrency, toCurrency) + } + conversionRate + } + + private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { + for (Map rates : DEFAULT_CURRENCY_RATES.values()) { + def fromRate = rates?[fromCurrency] + def toRate = rates?[toCurrency] + + if (fromRate && toRate) { + return toRate / fromRate + } + } + + null + } + + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice * adjustedValue, BID_ADJUST_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice - adjustedValue, BID_ADJUST_PRECISION) + case STATIC: + return adjustedValue + default: + return originalPrice + } } } diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java index 8b7dd0589b0..9bfbc9cb143 100644 --- a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -1,9 +1,7 @@ package org.prebid.server.auction; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,35 +9,26 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; -import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.validation.ResponseBidValidator; import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; -import java.util.EnumMap; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.Function; import java.util.function.UnaryOperator; import static java.util.Collections.emptyMap; @@ -48,16 +37,10 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; -import static org.prebid.server.proto.openrtb.ext.response.BidType.video; -import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @ExtendWith(MockitoExtension.class) public class BidsAdjusterTest extends VertxTest { @@ -65,9 +48,6 @@ public class BidsAdjusterTest extends VertxTest { @Mock(strictness = LENIENT) private ResponseBidValidator responseBidValidator; - @Mock(strictness = LENIENT) - private CurrencyConversionService currencyService; - @Mock(strictness = LENIENT) private PriceFloorEnforcer priceFloorEnforcer; @@ -75,7 +55,7 @@ public class BidsAdjusterTest extends VertxTest { private DsaEnforcer dsaEnforcer; @Mock(strictness = LENIENT) - private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private BidAdjustmentsProcessor bidAdjustmentsProcessor; private BidsAdjuster target; @@ -83,696 +63,51 @@ public class BidsAdjusterTest extends VertxTest { public void setUp() { given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); - given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null); - - givenTarget(); - } - - private void givenTarget() { - target = new BidsAdjuster( - responseBidValidator, - currencyService, - bidAdjustmentFactorResolver, - priceFloorEnforcer, - dsaEnforcer, - jacksonMapper); - } - - @Test - public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { - // given - final BidderResponse bidderResponse = givenBidderResponse( - Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(updatedPrice); - } - - @Test - public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { - // given - final BidderResponse bidderResponse = givenBidderResponse( - Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocation -> invocation.getArgument(0)); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2.0)); - } - - @Test - public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { - // given - final BidderResponse bidderResponse = givenBidderResponse( - Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD"); - final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).isEmpty(); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() { - // given - final BidderResponse bidderResponse = givenBidderResponse( - Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", BigDecimal.TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.TEN); - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(BigDecimal.TEN); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + .willAnswer(inv -> inv.getArgument(0)); - // then - final BigDecimal updatedPrice = BigDecimal.valueOf(100); - final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).isEmpty(); + target = new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); } @Test - public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { + public void shouldReturnBidsAdjustedByBidAdjustmentsProcessor() { // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), - givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2") - )) - .build(), - 1); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) - .willThrow( - new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); - - assertThat(result).hasSize(1); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "CUR1"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1"); - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); - - final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { - // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"), - givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR") - )) - .build(), - 1); - - final BidRequest bidRequest = BidRequest.builder() - .cur(singletonList("BAD")) - .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), - identity()))).build(); - - final BigDecimal updatedPrice = BigDecimal.valueOf(20); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD"))) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD")); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD")); - - assertThat(result).hasSize(1); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "USD"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD"); - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsOnly(expectedBidderBid); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD"); - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndAddWarningAboutMultipleCurrency() { - // given - final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder().impid("impId1").price(bidderPrice).build(), "USD") - )) - .build(), - 1); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1")); - - assertThat(result).hasSize(1); - - final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - - final BidderError expectedWarning = BidderError.badInput( - "a single currency (CUR1) has been chosen for the request. " - + "ORTB 2.6 requires that all responses are in the same currency."); - assertThat(firstSeatBid.getWarnings()).containsOnly(expectedWarning); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { - // given - final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); - final BigDecimal bidder2Price = BigDecimal.valueOf(2); - final BigDecimal bidder3Price = BigDecimal.valueOf(3); + final BidderBid bidToAdjust = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); final BidderResponse bidderResponse = BidderResponse.of( "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"), - givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"), - givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD") - )) - .build(), + BidderSeatBid.builder().bids(List.of(bidToAdjust)).build(), 1); final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(Map.of("bidder1", 1), identity())), - builder -> builder.cur(singletonList("USD"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); - verifyNoMoreInteractions(currencyService); - - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsOnly(bidder3Price, updatedPrice, updatedPrice); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { - // given - final BidderResponse bidderResponse = givenBidderResponse( - Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build()); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(2.468)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(4.936)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder() - .impid("123") - .price(BigDecimal.valueOf(2)).build(), - "USD", video) - )) - .build(), - 1); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(1).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder() - .impid("123") - .price(BigDecimal.valueOf(2)).build(), - "USD", video) - )) - .build(), - 1); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder() - .impid("123") - .price(BigDecimal.valueOf(2)).build(), - "USD", video) - )) - .build(), - 1); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder() - .impid("123") - .price(BigDecimal.valueOf(2)).build(), - "USD", video) - )) - .build(), - 1); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner), - givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative), - givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio))) - .build(), - 1); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - } - - @Test - public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder() - .impid("123") - .price(BigDecimal.valueOf(2)).build(), - "USD") - )) - .build(), - 1); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); - final AuctionContext auctionContext = givenAuctionContext(bidRequest); - - // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); - - // then - assertThat(result) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder", - BidderSeatBid.builder() - .bids(List.of( - givenBidderBid(Bid.builder() - .impid("123") - .price(BigDecimal.ONE).build(), - "USD") - )) - .build(), - 1); + List.of(givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + identity()); - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); + final BidderBid adjustedBid = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.TEN).build(), "USD"); - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .auctiontimestamp(1000L) - .currency(ExtRequestCurrency.of(null, false)) - .bidadjustmentfactors(givenAdjustments) - .build()))); + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(adjustedBid)), 0)) + .build()); final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); final AuctionContext auctionContext = givenAuctionContext(bidRequest); // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); + final List result = target.validateAndAdjustBids( + auctionParticipations, auctionContext, null); // then assertThat(result) .extracting(AuctionParticipation::getBidderResponse) .extracting(BidderResponse::getSeatBid) .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.ONE); + .containsExactly(adjustedBid); } @Test @@ -806,8 +141,8 @@ public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { final AuctionContext auctionContext = givenAuctionContext(bidRequest); // when - final List result = target - .validateAndAdjustBids(auctionParticipations, auctionContext, null); + final List result = target.validateAndAdjustBids( + auctionParticipations, auctionContext, null); // then assertThat(result) @@ -945,13 +280,37 @@ public void shouldTolerateResponseBidValidationWarnings() { "BidId `bidId1` validation messages: Warning: Error: bid validation warning.")); } - private BidderResponse givenBidderResponse(Bid bid) { - return BidderResponse.of( + @Test + public void shouldAddWarningAboutMultipleCurrency() { + // given + final BidderResponse bidderResponse = BidderResponse.of( "bidder", BidderSeatBid.builder() - .bids(singletonList(givenBidderBid(bid))) + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(BigDecimal.valueOf(2.0)).build(), + "CUR1"))) .build(), 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2"))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result).hasSize(1); + + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + final BidderError expectedWarning = BidderError.badInput( + "a single currency (CUR1) has been chosen for the request. " + + "ORTB 2.6 requires that all responses are in the same currency."); + assertThat(firstSeatBid.getWarnings()).containsOnly(expectedWarning); } private List givenAuctionParticipation( @@ -983,7 +342,7 @@ private static BidRequest givenBidRequest(List imp, .build(); } - private static Imp givenImp(T ext, Function impBuilderCustomizer) { + private static Imp givenImp(T ext, UnaryOperator impBuilderCustomizer) { return impBuilderCustomizer.apply(Imp.builder() .id(UUID.randomUUID().toString()) .ext(mapper.valueToTree(singletonMap( @@ -998,15 +357,4 @@ private static BidderBid givenBidderBid(Bid bid) { private static BidderBid givenBidderBid(Bid bid, String currency) { return BidderBid.of(bid, banner, currency); } - - private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) { - return BidderBid.of(bid, type, currency); - } - - private static Map doubleMap(K key1, V value1, K key2, V value2) { - final Map map = new HashMap<>(); - map.put(key1, value1); - map.put(key2, value2); - return map; - } } diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index dedb325e241..96889137331 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -36,6 +36,9 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.geolocation.model.GeoInfo; @@ -49,14 +52,18 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; import org.prebid.server.settings.model.Account; import java.util.ArrayList; +import java.util.List; +import java.util.Map; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -102,6 +109,8 @@ public class AuctionRequestFactoryTest extends VertxTest { private DebugResolver debugResolver; @Mock(strictness = LENIENT) private GeoLocationServiceWrapper geoLocationServiceWrapper; + @Mock(strictness = LENIENT) + private BidAdjustmentsRetriever bidAdjustmentsRetriever; private AuctionRequestFactory target; @@ -188,6 +197,7 @@ public void setUp() { .will(invocationOnMock -> invocationOnMock.getArgument(0)); given(geoLocationServiceWrapper.lookup(any())) .willReturn(Future.succeededFuture(GeoInfo.builder().vendor("vendor").build())); + given(bidAdjustmentsRetriever.retrieve(any())).willReturn(BidAdjustments.of(emptyMap())); target = new AuctionRequestFactory( Integer.MAX_VALUE, @@ -203,7 +213,8 @@ public void setUp() { auctionPrivacyContextFactory, debugResolver, jacksonMapper, - geoLocationServiceWrapper); + geoLocationServiceWrapper, + bidAdjustmentsRetriever); } @Test @@ -238,7 +249,8 @@ public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() { auctionPrivacyContextFactory, debugResolver, jacksonMapper, - geoLocationServiceWrapper); + geoLocationServiceWrapper, + bidAdjustmentsRetriever); given(routingContext.getBodyAsString()).willReturn("body"); @@ -714,6 +726,27 @@ public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementRetur assertThat(result.getGeoInfo()).isEqualTo(geoInfo); } + @Test + public void shouldReturnPopulatedBidAdjustments() { + // given + givenValidBidRequest(); + + final BidAdjustments bidAdjustments = BidAdjustments.of(Map.of( + "rule1", List.of( + ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()), + "rule2", List.of( + ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(), + ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build()))); + + given(bidAdjustmentsRetriever.retrieve(any())).willReturn(bidAdjustments); + + // when + final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); + + // then + assertThat(result.getBidAdjustments()).isEqualTo(bidAdjustments); + } + @Test public void shouldConvertBidRequestToInternalOpenRTBVersion() { // given diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java new file mode 100644 index 00000000000..0c98ff6af3b --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java @@ -0,0 +1,306 @@ +package org.prebid.server.bidadjustments; + +import org.junit.jupiter.api.Test; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.UNKNOWN; + +public class BidAdjustmentRulesValidatorTest { + + @Test + public void validateShouldDoNothingWhenBidAdjustmentsIsNull() throws ValidationException { + // when & then + BidAdjustmentRulesValidator.validate(null); + } + + @Test + public void validateShouldDoNothingWhenMediatypesIsEmpty() throws ValidationException { + // when & then + BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder().build()); + } + + @Test + public void validateShouldSkipMediatypeValidationWhenMediatypesIsNotSupported() throws ValidationException { + // given + final ExtRequestBidAdjustmentsRule invalidRule = ExtRequestBidAdjustmentsRule.builder() + .value(new BigDecimal("-999")) + .build(); + + // when & then + BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() + .mediatype(Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule))))) + .build()); + } + + @Test + public void validateShouldFailWhenBiddersAreAbsent() { + // given + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Collections.emptyMap())) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("no bidders found in banner"); + } + + @Test + public void validateShouldFailWhenDealsAreAbsent() { + // given + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", Collections.emptyMap()))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("no deals found in banner.bidderName"); + } + + @Test + public void validateShouldFailWhenRulesIsEmpty() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", null); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("no bid adjustment rules found in banner.bidderName.*"); + } + + @Test + public void validateShouldDoNothingWhenRulesAreEmpty() throws ValidationException { + // when & then + BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() + .mediatype(Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of())))) + .build()); + } + + @Test + public void validateShouldFailWhenRuleHasUnknownType() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(UNKNOWN) + .value(BigDecimal.ONE) + .currency("USD") + .build())); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=UNKNOWN, value=1, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenCpm("1", "USD"), givenCpm("1", null))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=CPM, value=1, currency=null] in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("-1", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=CPM, value=-1, currency=USD] in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("2147483647", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=CPM, value=2147483647, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenStatic("1", "USD"), givenStatic("1", null))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=STATIC, value=1, currency=null] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("-1", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=STATIC, value=-1, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("2147483647", "USD"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=STATIC, value=2147483647, currency=USD] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("-1"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=MULTIPLIER, value=-1, currency=null] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() { + // given + final Map> rules = new HashMap<>(); + rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("100"))); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of("banner", Map.of("bidderName", rules))) + .build(); + + // when & then + assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) + .isInstanceOf(ValidationException.class) + .hasMessage("the found rule [adjtype=MULTIPLIER, value=100, currency=null] " + + "in banner.bidderName.* is invalid"); + } + + @Test + public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationException { + // given + final List givenRules = List.of( + givenMultiplier("1"), + givenCpm("2", "USD"), + givenStatic("3", "EUR")); + + final Map>> givenRulesMap = Map.of( + "bidderName", + Map.of("dealId", givenRules)); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of( + "audio", givenRulesMap, + "native", givenRulesMap, + "video-instream", givenRulesMap, + "video-outstream", givenRulesMap, + "banner", givenRulesMap, + "video", givenRulesMap, + "unknown", givenRulesMap, + "*", Map.of( + "*", Map.of("*", givenRules), + "bidderName", Map.of( + "*", givenRules, + "dealId", givenRules)))) + .build(); + + //when & then + BidAdjustmentRulesValidator.validate(givenBidAdjustments); + } + + private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java new file mode 100644 index 00000000000..33cfe50e09c --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java @@ -0,0 +1,823 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +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.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +@ExtendWith(MockitoExtension.class) +public class BidAdjustmentsProcessorTest extends VertxTest { + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyService; + @Mock(strictness = LENIENT) + private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + @Mock(strictness = LENIENT) + private BidAdjustmentsResolver bidAdjustmentsResolver; + + private BidAdjustmentsProcessor target; + + @BeforeEach + public void before() { + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + + target = new BidAdjustmentsProcessor( + currencyService, + bidAdjustmentFactorResolver, + bidAdjustmentsResolver, + jacksonMapper); + } + + @Test + public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice); + + final BigDecimal expectedPrice = new BigDecimal("123.5"); + given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBidCurrency, bidderBid -> bidderBid.getBid().getPrice()) + .containsExactly(tuple("UAH", expectedPrice)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(2.0))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2.0)); + } + + @Test + public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + final BidderError expectedError = BidderError.generic( + "Unable to convert bid currency CUR to desired ad server currency USD"); + final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).isEmpty(); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + final BidderError expectedError = BidderError.generic( + "Unable to convert bid currency CUR to desired ad server currency USD"); + final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).isEmpty(); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAndBidAdjustments() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.TEN); + final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice); + final BigDecimal expectedPrice = new BigDecimal("123.5"); + given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + final BidderSeatBid seatBid = result.getBidderResponse().getSeatBid(); + assertThat(seatBid.getBids()) + .extracting(BidderBid::getBidCurrency, bidderBid -> bidderBid.getBid().getPrice()) + .containsExactly(tuple("UAH", expectedPrice)); + assertThat(seatBid.getErrors()).isEmpty(); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(20.0))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) + .willThrow( + new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, null); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "CUR1"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "UAH"); + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); + + final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "EUR") + )) + .build(), + 1); + + final BidRequest bidRequest = BidRequest.builder() + .cur(singletonList("CUR")) + .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), + identity()))).build(); + + final BigDecimal updatedPrice = BigDecimal.valueOf(20); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("CUR"))) + .willThrow(new PreBidException("Unable to convert bid currency EUR to desired ad server currency CUR")); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("CUR")); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("EUR"), eq("CUR")); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "USD"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR"); + assertThat(result.getBidderResponse().getSeatBid().getBids()).containsOnly(expectedBidderBid); + + final BidderError expectedError = BidderError.generic( + "Unable to convert bid currency EUR to desired ad server currency CUR"); + assertThat(result.getBidderResponse().getSeatBid().getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { + // given + final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); + final BigDecimal bidder2Price = BigDecimal.valueOf(2); + final BigDecimal bidder3Price = BigDecimal.valueOf(3); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"), + givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"), + givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(Map.of("bidder1", 1), identity())), + builder -> builder.cur(singletonList("USD"))); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); + verifyNoMoreInteractions(currencyService); + + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsOnly(bidder3Price, updatedPrice, updatedPrice); + + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any()); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).dealid("dealId").build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(2.468)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(4.936)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(4.936))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(1).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_instream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_instream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("125") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(2))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_instream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(2))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_outstream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); + + verify(bidAdjustmentsResolver, times(3)) + .resolve(any(), any(), any(), any(), any(), any()); + } + + @Test + public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD"))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsOnly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.ONE) + .dealid("dealId") + .build(), + "USD"))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .auctiontimestamp(1000L) + .currency(ExtRequestCurrency.of(null, false)) + .bidadjustmentfactors(givenAdjustments) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target + .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.ONE); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.ONE)), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.banner), + eq("bidder"), + eq("dealId")); + } + + private static BidRequest givenBidRequest(List imp, + UnaryOperator bidRequestBuilderCustomizer) { + + return bidRequestBuilderCustomizer + .apply(BidRequest.builder().cur(singletonList("UAH")).imp(imp).tmax(500L)) + .build(); + } + + private static Imp givenImp(T ext, UnaryOperator impBuilderCustomizer) { + return impBuilderCustomizer.apply(Imp.builder() + .id(UUID.randomUUID().toString()) + .ext(mapper.valueToTree(singletonMap( + "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap())))) + .build(); + } + + private static BidderBid givenBidderBid(Bid bid, String currency) { + return BidderBid.of(bid, banner, currency); + } + + private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) { + return BidderBid.of(bid, type, currency); + } + + private static Map doubleMap(K key1, V value1, K key2, V value2) { + final Map map = new HashMap<>(); + map.put(key1, value1); + map.put(key2, value2); + return map; + } + + private static BidAdjustments givenBidAdjustments() { + return BidAdjustments.of(ExtRequestBidAdjustments.builder().build()); + } + + private BidderResponse givenBidderResponse(Bid bid) { + return BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(singletonList(givenBidderBid(bid, "USD"))) + .build(), + 1); + } + + private AuctionParticipation givenAuctionParticipation(BidderResponse bidderResponse, + BidRequest bidRequest) { + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidRequest(bidRequest) + .build(); + + return AuctionParticipation.builder() + .bidder("bidder") + .bidderRequest(bidderRequest) + .bidderResponse(bidderResponse) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java new file mode 100644 index 00000000000..97ca68e939e --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java @@ -0,0 +1,243 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +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.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; + +@ExtendWith(MockitoExtension.class) +public class BidAdjustmentsResolverTest extends VertxTest { + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyService; + + private BidAdjustmentsResolver target; + + @BeforeEach + public void before() { + target = new BidAdjustmentsResolver(currencyService); + + given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> { + final BigDecimal initialPrice = (BigDecimal) invocation.getArguments()[0]; + return initialPrice.multiply(BigDecimal.TEN); + }); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificMediaType() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "banner|*|*", List.of(givenStatic("15", "EUR")), + "*|*|*", List.of(givenStatic("25", "UAH")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("EUR", new BigDecimal("15"))); + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardMediaType() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "banner|*|*", List.of(givenCpm("15", "EUR")), + "*|*|*", List.of(givenCpm("25", "UAH")))); + + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.video_outstream, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-249"))); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD"); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidder() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|bidderName|*", List.of(givenMultiplier("15")), + "*|*|*", List.of(givenMultiplier("25")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("15"))); + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardBidder() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|bidderName|*", List.of(givenStatic("15", "EUR"), givenMultiplier("15")), + "*|*|*", List.of(givenStatic("25", "UAH"), givenMultiplier("25")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "anotherBidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("UAH", new BigDecimal("625"))); + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificDealId() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR")), + "*|*|*", List.of(givenCpm("25", "JPY"), givenStatic("25", "UAH")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).isEqualTo(Price.of("EUR", new BigDecimal("15"))); + verify(currencyService).convertCurrency(new BigDecimal("15"), givenBidRequest, "JPY", "USD"); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealId() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenMultiplier("15"), givenCpm("15", "EUR")), + "*|*|*", List.of(givenMultiplier("25"), givenCpm("25", "UAH")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + "anotherDealId"); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-225"))); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD"); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenCpm("15", "EUR"), givenCpm("15", "JPY")), + "*|*|*", List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + givenBidRequest, + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + null); + + // then + assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-499"))); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD"); + verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "JPY", "USD"); + } + + @Test + public void resolveShouldReturnEmptyListWhenNoMatchFound() { + // given + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( + "*|*|dealId", List.of(givenStatic("15", "EUR")))); + + // when + final Price actual = target.resolve( + Price.of("USD", BigDecimal.ONE), + BidRequest.builder().build(), + givenBidAdjustments, + ImpMediaType.banner, + "bidderName", + null); + + // then + assertThat(actual).isEqualTo(Price.of("USD", BigDecimal.ONE)); + verifyNoInteractions(currencyService); + } + + private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java new file mode 100644 index 00000000000..df6caa05abd --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java @@ -0,0 +1,396 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.debug.DebugContext; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; + +public class BidAdjustmentsRetrieverTest extends VertxTest { + + private BidAdjustmentsRetriever target; + + @BeforeEach + public void before() { + target = new BidAdjustmentsRetriever(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d); + } + + @Test + public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() { + // given + final List debugMessages = new ArrayList<>(); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + null, null, debugMessages, true)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountAdjustmentsAreAbsent() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "invalid": { + "invalid": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, null, debugMessages, true)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages) + .containsOnly("bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.invalid.invalid is invalid"); + } + + @Test + public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAbsent() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, null, debugMessages, true)); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "banner|*|*", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal("0.1")) + .build()))); + + assertThat(actual).isEqualTo(expected); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAbsent() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "audio": { + "bidder": { + "*": [ + { + "adjtype": "static", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "audio|bidder|*", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(STATIC) + .currency("USD") + .value(new BigDecimal("0.1")) + .build()))); + + assertThat(actual).isEqualTo(expected); + assertThat(debugMessages) + .containsOnly("bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.*.* is invalid"); + } + + @Test + public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustmentsAreInvalid() + throws JsonProcessingException { + + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "audio": { + "bidder": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages).containsExactlyInAnyOrder( + "bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid", + "bid adjustment from account was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid"); + } + + @Test + public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException { + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "audio": { + "bidder": { + "*": [ + { + "adjtype": "invalid", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, false)); + + // then + assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(debugMessages).isEmpty(); + } + + @Test + public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws JsonProcessingException { + // given + final List debugMessages = new ArrayList<>(); + final String requestAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": 0.1, + "currency": "USD" + } + ] + } + } + } + } + """; + + final String accountAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "dealId": [ + { + "adjtype": "cpm", + "value": 0.3, + "currency": "USD" + } + ], + "*": [ + { + "adjtype": "static", + "value": 0.2, + "currency": "USD" + } + ] + } + } + } + } + """; + + final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); + final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); + + // when + final BidAdjustments actual = target.retrieve(givenAuctionContext( + givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "banner|*|dealId", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal("0.3")) + .build()), + "banner|*|*", + List.of(ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal("0.1")) + .build()))); + + assertThat(actual).isEqualTo(expected); + assertThat(debugMessages).isEmpty(); + } + + private static AuctionContext givenAuctionContext(ObjectNode requestBidAdjustments, + ObjectNode accountBidAdjustments, + List debugWarnings, + boolean debugEnabled) { + + return AuctionContext.builder() + .debugContext(DebugContext.of(debugEnabled, false, null)) + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(requestBidAdjustments).build())) + .build()) + .account(Account.builder() + .auction(AccountAuctionConfig.builder().bidAdjustments(accountBidAdjustments).build()) + .build()) + .debugWarnings(debugWarnings) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java new file mode 100644 index 00000000000..6bc26d7ef1a --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java @@ -0,0 +1,65 @@ +package org.prebid.server.bidadjustments.model; + +import org.junit.jupiter.api.Test; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; + +public class BidAdjustmentsTest { + + @Test + public void shouldBuildRulesSet() { + // given + final List givenRules = List.of(givenRule("1"), givenRule("2")); + final Map>> givenRulesMap = Map.of( + "bidderName", + Map.of("dealId", givenRules)); + + final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() + .mediatype(Map.of( + "audio", givenRulesMap, + "native", givenRulesMap, + "video-instream", givenRulesMap, + "video-outstream", givenRulesMap, + "banner", givenRulesMap, + "video", givenRulesMap, + "unknown", givenRulesMap, + "*", Map.of( + "*", Map.of("*", givenRules), + "bidderName", Map.of( + "*", givenRules, + "dealId", givenRules)))) + .build(); + + // when + final BidAdjustments actual = BidAdjustments.of(givenBidAdjustments); + + // then + final BidAdjustments expected = BidAdjustments.of(Map.of( + "audio|bidderName|dealId", givenRules, + "native|bidderName|dealId", givenRules, + "video-instream|bidderName|dealId", givenRules, + "video-outstream|bidderName|dealId", givenRules, + "banner|bidderName|dealId", givenRules, + "*|*|*", givenRules, + "*|bidderName|*", givenRules, + "*|bidderName|dealId", givenRules)); + + assertThat(actual).isEqualTo(expected); + + } + + private static ExtRequestBidAdjustmentsRule givenRule(String value) { + return ExtRequestBidAdjustmentsRule.builder() + .adjType(CPM) + .currency("USD") + .value(new BigDecimal(value)) + .build(); + } +} From e11731e80b0034e51f4910e3a0d162db98ac0caa Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 15 Nov 2024 15:19:37 +0100 Subject: [PATCH 126/170] Prebid Server prepare release 3.15.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 6 ++---- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 13 insertions(+), 15 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 74257f0b3cd..e17a781d7b6 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.15.0-SNAPSHOT + 3.15.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index a4b77048c76..6f299f390ec 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 963b239763e..b23cb84add7 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index 2d6bf82383a..ffb5895eec4 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 90fe75bac96..1951dfb384f 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index 1686cadfaac..a7bda0b4c86 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 802bed7fe04..d0a5212e863 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index fc852b520f2..ba258f7ffd4 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0-SNAPSHOT + 3.15.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index d40b7e7829e..d414ca0f993 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.15.0-SNAPSHOT + 3.15.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 6fe748f904a..ae1cd1010b9 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.15.0-SNAPSHOT + 3.15.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.15.0 diff --git a/pom.xml b/pom.xml index e674293243e..3ab1260513c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.15.0-SNAPSHOT + 3.15.0 extra/pom.xml From 15f19756162eb60e2edb5a017393a61bdf7eff1f Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Fri, 15 Nov 2024 15:19:37 +0100 Subject: [PATCH 127/170] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index e17a781d7b6..12149265f3e 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.15.0 + 3.16.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 6f299f390ec..f4f0a90ff01 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index b23cb84add7..b4a290dd118 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index ffb5895eec4..75253e93147 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 1951dfb384f..fa56eb6a663 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index a7bda0b4c86..e48517b0eda 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index d0a5212e863..43af330f26b 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index ba258f7ffd4..df4c41dece4 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.15.0 + 3.16.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index d414ca0f993..ad0987866f5 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.15.0 + 3.16.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index ae1cd1010b9..0e279f95f98 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.15.0 + 3.16.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.15.0 + HEAD diff --git a/pom.xml b/pom.xml index 3ab1260513c..b203bcab93e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.15.0 + 3.16.0-SNAPSHOT extra/pom.xml From 107efeb0e4f61aa20a1a523f2fb83f8d255be160 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:23:15 +0100 Subject: [PATCH 128/170] Core: Refactor file syncer (#3539) --- .../v1/model/BidderInvocationContextImpl.java | 2 +- .../server/auction/BidResponseCreator.java | 2 +- .../server/auction/ExchangeService.java | 4 +- .../auction/GeoLocationServiceWrapper.java | 2 +- .../server/auction/SkippedAuctionService.java | 2 +- .../auction/StoredRequestProcessor.java | 4 +- .../auction/StoredResponseProcessor.java | 2 +- .../auction/VideoStoredRequestProcessor.java | 2 +- .../BasicCategoryMappingService.java | 2 +- .../CategoryMappingService.java | 2 +- .../NoOpCategoryMappingService.java | 2 +- .../server/auction/model/SetuidContext.java | 2 +- .../server/auction/model/TimeoutContext.java | 2 +- .../CookieSyncPrivacyContextFactory.java | 2 +- .../SetuidPrivacyContextFactory.java | 2 +- .../requestfactory/Ortb2RequestFactory.java | 4 +- .../server/bidder/HttpBidderRequester.java | 2 +- .../prebid/server/cache/CoreCacheService.java | 2 +- .../cookie/model/CookieSyncContext.java | 2 +- .../server/execution/RemoteFileProcessor.java | 12 - .../server/execution/file/FileProcessor.java | 8 + .../server/execution/file/FileUtil.java | 106 ++++++++ .../file/supplier/LocalFileSupplier.java | 47 ++++ .../file/supplier/RemoteFileSupplier.java | 160 ++++++++++++ .../execution/file/syncer/FileSyncer.java | 84 ++++++ .../file/syncer/LocalFileSyncer.java | 38 +++ .../{ => file/syncer}/RemoteFileSyncer.java | 36 +-- .../file/syncer/RemoteFileSyncerV2.java | 69 +++++ .../execution/{ => timeout}/Timeout.java | 2 +- .../{ => timeout}/TimeoutFactory.java | 2 +- .../server/floors/PriceFloorFetcher.java | 2 +- ...rcuitBreakerSecuredGeoLocationService.java | 2 +- .../ConfigurationGeoLocationService.java | 2 +- .../geolocation/GeoLocationService.java | 2 +- .../MaxMindGeoLocationService.java | 6 +- .../server/handler/CookieSyncHandler.java | 4 +- .../handler/NotificationEventHandler.java | 2 +- .../prebid/server/handler/SetuidHandler.java | 4 +- .../prebid/server/handler/VtrackHandler.java | 4 +- .../health/GeoLocationHealthChecker.java | 2 +- .../hooks/execution/HookStageExecutor.java | 4 +- .../execution/v1/InvocationContextImpl.java | 2 +- .../server/hooks/v1/InvocationContext.java | 2 +- .../privacy/gdpr/TcfDefinerService.java | 2 +- .../server/settings/ApplicationSettings.java | 2 +- .../settings/CachingApplicationSettings.java | 2 +- .../CompositeApplicationSettings.java | 2 +- .../settings/DatabaseApplicationSettings.java | 2 +- .../EnrichingApplicationSettings.java | 2 +- .../settings/FileApplicationSettings.java | 2 +- .../settings/HttpApplicationSettings.java | 2 +- .../settings/S3ApplicationSettings.java | 2 +- .../DatabasePeriodicRefreshService.java | 4 +- .../config/GeoLocationConfiguration.java | 63 +---- .../config/HealthCheckerConfiguration.java | 2 +- .../spring/config/HooksConfiguration.java | 2 +- .../config/PriceFloorsConfiguration.java | 2 +- .../spring/config/ServiceConfiguration.java | 2 +- .../spring/config/SettingsConfiguration.java | 2 +- ...perties.java => FileSyncerProperties.java} | 11 +- .../ApplicationServerConfiguration.java | 2 +- .../vertx/database/BasicDatabaseClient.java | 2 +- .../CircuitBreakerSecuredDatabaseClient.java | 2 +- .../server/vertx/database/DatabaseClient.java | 2 +- .../pubstack/PubstackEventHandlerTest.java | 2 +- .../BasicCategoryMappingServiceTest.java | 4 +- .../auction/BidResponseCreatorTest.java | 4 +- .../server/auction/ExchangeServiceTest.java | 4 +- .../GeoLocationServiceWrapperTest.java | 4 +- .../auction/SkippedAuctionServiceTest.java | 2 +- .../auction/StoredRequestProcessorTest.java | 2 +- .../auction/StoredResponseProcessorTest.java | 4 +- .../VideoStoredRequestProcessorTest.java | 2 +- .../CookieSyncPrivacyContextFactoryTest.java | 2 +- .../SetuidPrivacyContextFactoryTest.java | 2 +- .../Ortb2RequestFactoryTest.java | 4 +- .../bidder/HttpBidderRequesterTest.java | 4 +- .../server/cache/CoreCacheServiceTest.java | 4 +- .../file/supplier/LocalFileSupplierTest.java | 68 +++++ .../file/supplier/RemoteFileSupplierTest.java | 244 ++++++++++++++++++ .../execution/file/syncer/FileSyncerTest.java | 160 ++++++++++++ .../syncer}/RemoteFileSyncerTest.java | 74 +++--- .../{ => timeout}/TimeoutFactoryTest.java | 2 +- .../execution/{ => timeout}/TimeoutTest.java | 2 +- .../server/floors/PriceFloorFetcherTest.java | 2 +- .../ConfigurationGeoLocationServiceTest.java | 2 +- .../server/handler/CookieSyncHandlerTest.java | 2 +- .../handler/NotificationEventHandlerTest.java | 2 +- .../server/handler/SetuidHandlerTest.java | 2 +- .../server/handler/VtrackHandlerTest.java | 2 +- .../handler/openrtb2/AmpHandlerTest.java | 4 +- .../handler/openrtb2/AuctionHandlerTest.java | 4 +- .../handler/openrtb2/VideoHandlerTest.java | 4 +- .../health/GeoLocationHealthCheckerTest.java | 2 +- .../execution/HookStageExecutorTest.java | 2 +- .../CachingApplicationSettingsTest.java | 4 +- .../DatabaseApplicationSettingsTest.java | 4 +- .../EnrichingApplicationSettingsTest.java | 2 +- .../settings/HttpApplicationSettingsTest.java | 4 +- .../settings/S3ApplicationSettingsTest.java | 2 +- .../DatabasePeriodicRefreshServiceTest.java | 2 +- .../database/BasicDatabaseClientTest.java | 4 +- ...rcuitBreakerSecuredDatabaseClientTest.java | 4 +- 103 files changed, 1172 insertions(+), 236 deletions(-) delete mode 100644 src/main/java/org/prebid/server/execution/RemoteFileProcessor.java create mode 100644 src/main/java/org/prebid/server/execution/file/FileProcessor.java create mode 100644 src/main/java/org/prebid/server/execution/file/FileUtil.java create mode 100644 src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java create mode 100644 src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java create mode 100644 src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java create mode 100644 src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java rename src/main/java/org/prebid/server/execution/{ => file/syncer}/RemoteFileSyncer.java (84%) create mode 100644 src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java rename src/main/java/org/prebid/server/execution/{ => timeout}/Timeout.java (95%) rename src/main/java/org/prebid/server/execution/{ => timeout}/TimeoutFactory.java (95%) rename src/main/java/org/prebid/server/spring/config/model/{RemoteFileSyncerProperties.java => FileSyncerProperties.java} (83%) create mode 100644 src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java create mode 100644 src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java create mode 100644 src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java rename src/test/java/org/prebid/server/execution/{ => file/syncer}/RemoteFileSyncerTest.java (87%) rename src/test/java/org/prebid/server/execution/{ => timeout}/TimeoutFactoryTest.java (97%) rename src/test/java/org/prebid/server/execution/{ => timeout}/TimeoutTest.java (95%) diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java index 8b68c9279df..d39d0a4ca5f 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java @@ -7,7 +7,7 @@ import lombok.experimental.Accessors; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRejectionTracker; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.model.Endpoint; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 1c0b837bb4e..31ed4ee1403 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -52,7 +52,7 @@ import org.prebid.server.events.EventsService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 27c65afe260..7e37c48f219 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -57,8 +57,8 @@ import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; diff --git a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java index 4c1a3b0b8cf..609e7481b81 100644 --- a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java +++ b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java index dd8c95c0f50..e833b317cc2 100644 --- a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java +++ b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java index f982870c049..3729c5da661 100644 --- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java @@ -12,8 +12,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.InvalidStoredImpException; import org.prebid.server.exception.InvalidStoredRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java index b769d2974b1..1f5b0d83258 100644 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java @@ -21,7 +21,7 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index 84ebacc7416..f6bcb5599af 100644 --- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java @@ -24,7 +24,7 @@ import org.prebid.server.auction.model.Tuple2; import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java index b13cf522b49..e9a7b7818a1 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -28,7 +28,7 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtDealTier; diff --git a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java index 088a9604b9d..2c3b3f369b0 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java @@ -4,7 +4,7 @@ import io.vertx.core.Future; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import java.util.List; diff --git a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java index f6161fa6f90..88ac988c521 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java @@ -4,7 +4,7 @@ import io.vertx.core.Future; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import java.util.List; diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java index d045b0ceadc..05c451bf863 100644 --- a/src/main/java/org/prebid/server/auction/model/SetuidContext.java +++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.bidder.UsersyncMethodType; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java index b8379056afa..87c390e6b2e 100644 --- a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java +++ b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java @@ -1,7 +1,7 @@ package org.prebid.server.auction.model; import lombok.Value; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; @Value(staticConstructor = "of") public class TimeoutContext { diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java index 55e32fd1372..0e1e6cbab13 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java @@ -6,7 +6,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.metric.MetricName; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java index 3e81b3fcf4f..b637e656290 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java @@ -6,7 +6,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.metric.MetricName; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java index 336f2b5f8f1..90b20dd4f29 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java @@ -33,8 +33,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index aa917b017cb..787ee1f96d2 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -24,7 +24,7 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index 5d5034e23ce..e60ed70f949 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -27,7 +27,7 @@ import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.identity.UUIDIdGenerator; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; diff --git a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java index 886c245122c..281313d25f5 100644 --- a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java +++ b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.bidder.UsersyncMethodChooser; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java b/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java deleted file mode 100644 index 8621e00dbce..00000000000 --- a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.execution; - -import io.vertx.core.Future; - -/** - * Contract fro services which use external files. - */ -public interface RemoteFileProcessor { - - Future setDataPath(String dataFilePath); -} - diff --git a/src/main/java/org/prebid/server/execution/file/FileProcessor.java b/src/main/java/org/prebid/server/execution/file/FileProcessor.java new file mode 100644 index 00000000000..f17ab4758ee --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/FileProcessor.java @@ -0,0 +1,8 @@ +package org.prebid.server.execution.file; + +import io.vertx.core.Future; + +public interface FileProcessor { + + Future setDataPath(String dataFilePath); +} diff --git a/src/main/java/org/prebid/server/execution/file/FileUtil.java b/src/main/java/org/prebid/server/execution/file/FileUtil.java new file mode 100644 index 00000000000..3de28c1992f --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/FileUtil.java @@ -0,0 +1,106 @@ +package org.prebid.server.execution.file; + +import io.vertx.core.Vertx; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.file.FileSystemException; +import io.vertx.core.http.HttpClientOptions; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.syncer.FileSyncer; +import org.prebid.server.execution.file.syncer.LocalFileSyncer; +import org.prebid.server.execution.file.syncer.RemoteFileSyncerV2; +import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; +import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; +import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.spring.config.model.ExponentialBackoffProperties; +import org.prebid.server.spring.config.model.FileSyncerProperties; +import org.prebid.server.spring.config.model.HttpClientProperties; + +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileUtil { + + private FileUtil() { + } + + public static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { + try { + final Path dirPath = Paths.get(filePath).getParent(); + final String dirPathString = dirPath.toString(); + final FileProps props = fileSystem.existsBlocking(dirPathString) + ? fileSystem.propsBlocking(dirPathString) + : null; + + if (props == null || !props.isDirectory()) { + fileSystem.mkdirsBlocking(dirPathString); + } else if (!Files.isWritable(dirPath)) { + throw new PreBidException("No write permissions for directory: " + dirPath); + } + } catch (FileSystemException | InvalidPathException e) { + throw new PreBidException("Cannot create directory for file: " + filePath, e); + } + } + + public static FileSyncer fileSyncerFor(FileProcessor fileProcessor, + FileSyncerProperties properties, + Vertx vertx) { + + return switch (properties.getType()) { + case LOCAL -> new LocalFileSyncer( + fileProcessor, + properties.getSaveFilepath(), + properties.getUpdateIntervalMs(), + toRetryPolicy(properties), + vertx); + case REMOTE -> remoteFileSyncer(fileProcessor, properties, vertx); + }; + } + + private static RemoteFileSyncerV2 remoteFileSyncer(FileProcessor fileProcessor, + FileSyncerProperties properties, + Vertx vertx) { + + final HttpClientProperties httpClientProperties = properties.getHttpClient(); + final HttpClientOptions httpClientOptions = new HttpClientOptions() + .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) + .setMaxRedirects(httpClientProperties.getMaxRedirects()); + + return new RemoteFileSyncerV2( + fileProcessor, + properties.getDownloadUrl(), + properties.getSaveFilepath(), + properties.getTmpFilepath(), + vertx.createHttpClient(httpClientOptions), + properties.getTimeoutMs(), + properties.isCheckSize(), + properties.getUpdateIntervalMs(), + toRetryPolicy(properties), + vertx); + } + + // TODO: remove after transition period + private static RetryPolicy toRetryPolicy(FileSyncerProperties properties) { + final Long retryIntervalMs = properties.getRetryIntervalMs(); + final Integer retryCount = properties.getRetryCount(); + final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount); + final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount) + || !fixedRetryPolicyDefined; + + if (!fixedRetryPolicyValid) { + throw new IllegalArgumentException("fixed interval retry policy is invalid"); + } + + final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry(); + return fixedRetryPolicyDefined + ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount) + : ExponentialBackoffRetryPolicy.of( + exponentialBackoffProperties.getDelayMillis(), + exponentialBackoffProperties.getMaxDelayMillis(), + exponentialBackoffProperties.getFactor(), + exponentialBackoffProperties.getJitter()); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java new file mode 100644 index 00000000000..55517caa9a7 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java @@ -0,0 +1,47 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +public class LocalFileSupplier implements Supplier> { + + private final String filePath; + private final FileSystem fileSystem; + private final AtomicLong lastSupplyTime; + + public LocalFileSupplier(String filePath, FileSystem fileSystem) { + this.filePath = Objects.requireNonNull(filePath); + this.fileSystem = Objects.requireNonNull(fileSystem); + lastSupplyTime = new AtomicLong(Long.MIN_VALUE); + } + + @Override + public Future get() { + return fileSystem.exists(filePath) + .compose(exists -> exists + ? fileSystem.props(filePath) + : Future.failedFuture("File %s not found.".formatted(filePath))) + .map(this::getFileIfModified); + } + + private String getFileIfModified(FileProps fileProps) { + final long lastModifiedTime = lasModifiedTime(fileProps); + final long lastSupplyTime = this.lastSupplyTime.get(); + + if (lastSupplyTime < lastModifiedTime) { + this.lastSupplyTime.compareAndSet(lastSupplyTime, lastModifiedTime); + return filePath; + } + + return null; + } + + private static long lasModifiedTime(FileProps fileProps) { + return Math.max(fileProps.creationTime(), fileProps.lastModifiedTime()); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java new file mode 100644 index 00000000000..e8b8f313c54 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java @@ -0,0 +1,160 @@ +package org.prebid.server.execution.file.supplier; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.file.CopyOptions; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.file.OpenOptions; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.util.HttpUtil; + +import java.util.Objects; +import java.util.function.Supplier; + +public class RemoteFileSupplier implements Supplier> { + + private static final Logger logger = LoggerFactory.getLogger(RemoteFileSupplier.class); + + private final String savePath; + private final String backupPath; + private final String tmpPath; + private final HttpClient httpClient; + private final FileSystem fileSystem; + + private final RequestOptions getRequestOptions; + private final RequestOptions headRequestOptions; + + public RemoteFileSupplier(String downloadUrl, + String savePath, + String tmpPath, + HttpClient httpClient, + long timeout, + boolean checkRemoteFileSize, + FileSystem fileSystem) { + + this.savePath = Objects.requireNonNull(savePath); + this.backupPath = savePath + ".old"; + this.tmpPath = Objects.requireNonNull(tmpPath); + this.httpClient = Objects.requireNonNull(httpClient); + this.fileSystem = Objects.requireNonNull(fileSystem); + + HttpUtil.validateUrl(downloadUrl); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, savePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, backupPath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpPath); + + getRequestOptions = new RequestOptions() + .setMethod(HttpMethod.GET) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); + headRequestOptions = checkRemoteFileSize + ? new RequestOptions() + .setMethod(HttpMethod.HEAD) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true) + : null; + } + + @Override + public Future get() { + return isDownloadRequired().compose(isDownloadRequired -> isDownloadRequired + ? Future.all(downloadFile(), createBackup()) + .compose(ignored -> tmpToSave()) + .map(savePath) + : Future.succeededFuture()); + } + + private Future isDownloadRequired() { + return headRequestOptions != null + ? fileSystem.exists(savePath) + .compose(exists -> exists ? isSizeChanged() : Future.succeededFuture(true)) + : Future.succeededFuture(true); + } + + private Future isSizeChanged() { + final Future localFileSize = fileSystem.props(savePath).map(FileProps::size); + final Future remoteFileSize = sendHttpRequest(headRequestOptions) + .map(response -> response.getHeader(HttpHeaders.CONTENT_LENGTH)) + .map(Long::parseLong); + + return Future.all(localFileSize, remoteFileSize) + .map(compositeResult -> !Objects.equals(compositeResult.resultAt(0), compositeResult.resultAt(1))); + } + + private Future downloadFile() { + return fileSystem.open(tmpPath, new OpenOptions()) + .compose(tmpFile -> sendHttpRequest(getRequestOptions) + .compose(response -> response.pipeTo(tmpFile)) + .onComplete(result -> tmpFile.close())); + } + + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .map(this::validateResponse); + } + + private HttpClientResponse validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + throw new PreBidException("Got unexpected response from server with status code %s and message %s" + .formatted(statusCode, response.statusMessage())); + } + + return response; + } + + private Future tmpToSave() { + return copyFile(tmpPath, savePath); + } + + public void clearTmp() { + fileSystem.exists(tmpPath).onSuccess(exists -> { + if (exists) { + deleteFile(tmpPath); + } + }); + } + + private Future createBackup() { + return fileSystem.exists(savePath) + .compose(exists -> exists ? copyFile(savePath, backupPath) : Future.succeededFuture()); + } + + public void deleteBackup() { + fileSystem.exists(backupPath).onSuccess(exists -> { + if (exists) { + deleteFile(backupPath); + } + }); + } + + public Future restoreFromBackup() { + return fileSystem.exists(backupPath) + .compose(exists -> exists + ? copyFile(backupPath, savePath) + .onSuccess(ignored -> deleteFile(backupPath)) + : Future.succeededFuture()); + } + + private Future copyFile(String from, String to) { + return fileSystem.move(from, to, new CopyOptions().setReplaceExisting(true)); + } + + private void deleteFile(String filePath) { + fileSystem.delete(filePath) + .onFailure(error -> logger.error("Can't delete file: " + filePath)); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java new file mode 100644 index 00000000000..fd850e126c4 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java @@ -0,0 +1,84 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.retry.Retryable; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; +import java.util.function.Function; + +public abstract class FileSyncer { + + private static final Logger logger = LoggerFactory.getLogger(FileSyncer.class); + + private final FileProcessor fileProcessor; + private final long updatePeriod; + private final RetryPolicy retryPolicy; + private final Vertx vertx; + + protected FileSyncer(FileProcessor fileProcessor, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + this.fileProcessor = Objects.requireNonNull(fileProcessor); + this.updatePeriod = updatePeriod; + this.retryPolicy = Objects.requireNonNull(retryPolicy); + this.vertx = Objects.requireNonNull(vertx); + } + + public void sync() { + sync(retryPolicy); + } + + private void sync(RetryPolicy currentRetryPolicy) { + getFile() + .compose(this::processFile) + .onSuccess(ignored -> onSuccess()) + .onFailure(failure -> onFailure(currentRetryPolicy, failure)); + } + + protected abstract Future getFile(); + + private Future processFile(String filePath) { + return filePath != null + ? vertx.executeBlocking(() -> fileProcessor.setDataPath(filePath)) + .compose(Function.identity()) + .onFailure(error -> logger.error("Can't process saved file: " + filePath)) + : Future.succeededFuture(); + } + + private void onSuccess() { + doOnSuccess().onComplete(ignored -> setUpDeferredUpdate()); + } + + protected abstract Future doOnSuccess(); + + private void setUpDeferredUpdate() { + if (updatePeriod > 0) { + vertx.setTimer(updatePeriod, ignored -> sync()); + } + } + + private void onFailure(RetryPolicy currentRetryPolicy, Throwable failure) { + doOnFailure(failure).onComplete(ignored -> retrySync(currentRetryPolicy)); + } + + protected abstract Future doOnFailure(Throwable throwable); + + private void retrySync(RetryPolicy currentRetryPolicy) { + if (currentRetryPolicy instanceof Retryable policy) { + logger.info( + "Retrying file sync for {} with policy: {}", + fileProcessor.getClass().getSimpleName(), + policy); + vertx.setTimer(policy.delay(), timerId -> sync(policy.next())); + } else { + setUpDeferredUpdate(); + } + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java new file mode 100644 index 00000000000..6ea109185b5 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java @@ -0,0 +1,38 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.supplier.LocalFileSupplier; +import org.prebid.server.execution.retry.RetryPolicy; + +public class LocalFileSyncer extends FileSyncer { + + private final LocalFileSupplier localFileSupplier; + + public LocalFileSyncer(FileProcessor fileProcessor, + String localFile, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + + localFileSupplier = new LocalFileSupplier(localFile, vertx.fileSystem()); + } + + @Override + protected Future getFile() { + return localFileSupplier.get(); + } + + @Override + protected Future doOnSuccess() { + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + return Future.succeededFuture(); + } +} diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java similarity index 84% rename from src/main/java/org/prebid/server/execution/RemoteFileSyncer.java rename to src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java index b841bf8a136..8deb838646f 100644 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java @@ -1,13 +1,11 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.file.syncer; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.file.CopyOptions; -import io.vertx.core.file.FileProps; import io.vertx.core.file.FileSystem; -import io.vertx.core.file.FileSystemException; import io.vertx.core.file.OpenOptions; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; @@ -17,22 +15,23 @@ import io.vertx.core.http.RequestOptions; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.FileUtil; import org.prebid.server.execution.retry.RetryPolicy; import org.prebid.server.execution.retry.Retryable; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Paths; import java.util.Objects; +import java.util.function.Function; +@Deprecated public class RemoteFileSyncer { private static final Logger logger = LoggerFactory.getLogger(RemoteFileSyncer.class); - private final RemoteFileProcessor processor; + private final FileProcessor processor; private final String downloadUrl; private final String saveFilePath; private final String tmpFilePath; @@ -44,7 +43,7 @@ public class RemoteFileSyncer { private final RequestOptions getFileRequestOptions; private final RequestOptions isUpdateRequiredRequestOptions; - public RemoteFileSyncer(RemoteFileProcessor processor, + public RemoteFileSyncer(FileProcessor processor, String downloadUrl, String saveFilePath, String tmpFilePath, @@ -64,8 +63,8 @@ public RemoteFileSyncer(RemoteFileProcessor processor, this.vertx = Objects.requireNonNull(vertx); this.fileSystem = vertx.fileSystem(); - createAndCheckWritePermissionsFor(fileSystem, saveFilePath); - createAndCheckWritePermissionsFor(fileSystem, tmpFilePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, saveFilePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpFilePath); getFileRequestOptions = new RequestOptions() .setMethod(HttpMethod.GET) @@ -80,20 +79,6 @@ public RemoteFileSyncer(RemoteFileProcessor processor, .setFollowRedirects(true); } - private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { - try { - final String dirPath = Paths.get(filePath).getParent().toString(); - final FileProps props = fileSystem.existsBlocking(dirPath) ? fileSystem.propsBlocking(dirPath) : null; - if (props == null || !props.isDirectory()) { - fileSystem.mkdirsBlocking(dirPath); - } else if (!Files.isWritable(Paths.get(dirPath))) { - throw new PreBidException("No write permissions for directory: " + dirPath); - } - } catch (FileSystemException | InvalidPathException e) { - throw new PreBidException("Cannot create directory for file: " + filePath, e); - } - } - public void sync() { fileSystem.exists(saveFilePath) .compose(exists -> exists ? processSavedFile() : syncRemoteFile(retryPolicy)) @@ -101,7 +86,8 @@ public void sync() { } private Future processSavedFile() { - return processor.setDataPath(saveFilePath) + return vertx.executeBlocking(() -> processor.setDataPath(saveFilePath)) + .compose(Function.identity()) .onFailure(error -> logger.error("Can't process saved file: " + saveFilePath)) .recover(ignored -> deleteFile(saveFilePath).mapEmpty()) .mapEmpty(); diff --git a/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java new file mode 100644 index 00000000000..54755dccc19 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java @@ -0,0 +1,69 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.file.FileSystem; +import io.vertx.core.http.HttpClient; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.supplier.LocalFileSupplier; +import org.prebid.server.execution.file.supplier.RemoteFileSupplier; +import org.prebid.server.execution.retry.RetryPolicy; + +public class RemoteFileSyncerV2 extends FileSyncer { + + private final LocalFileSupplier localFileSupplier; + private final RemoteFileSupplier remoteFileSupplier; + + public RemoteFileSyncerV2(FileProcessor fileProcessor, + String downloadUrl, + String saveFilePath, + String tmpFilePath, + HttpClient httpClient, + long timeout, + boolean checkSize, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + + final FileSystem fileSystem = vertx.fileSystem(); + localFileSupplier = new LocalFileSupplier(saveFilePath, fileSystem); + remoteFileSupplier = new RemoteFileSupplier( + downloadUrl, + saveFilePath, + tmpFilePath, + httpClient, + timeout, + checkSize, + fileSystem); + } + + @Override + protected Future getFile() { + return localFileSupplier.get() + .otherwiseEmpty() + .compose(localFile -> localFile != null + ? Future.succeededFuture(localFile) + : remoteFileSupplier.get()); + } + + @Override + protected Future doOnSuccess() { + remoteFileSupplier.clearTmp(); + remoteFileSupplier.deleteBackup(); + forceLastSupplyTimeUpdate(); + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + remoteFileSupplier.clearTmp(); + return remoteFileSupplier.restoreFromBackup() + .onSuccess(ignore -> forceLastSupplyTimeUpdate()); + } + + private void forceLastSupplyTimeUpdate() { + localFileSupplier.get(); + } +} diff --git a/src/main/java/org/prebid/server/execution/Timeout.java b/src/main/java/org/prebid/server/execution/timeout/Timeout.java similarity index 95% rename from src/main/java/org/prebid/server/execution/Timeout.java rename to src/main/java/org/prebid/server/execution/timeout/Timeout.java index f5abf239c87..b0f37e439fc 100644 --- a/src/main/java/org/prebid/server/execution/Timeout.java +++ b/src/main/java/org/prebid/server/execution/timeout/Timeout.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import lombok.Getter; diff --git a/src/main/java/org/prebid/server/execution/TimeoutFactory.java b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java similarity index 95% rename from src/main/java/org/prebid/server/execution/TimeoutFactory.java rename to src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java index cbe2768af1a..ae2624c8585 100644 --- a/src/main/java/org/prebid/server/execution/TimeoutFactory.java +++ b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import java.time.Clock; diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index 0692dce090a..0195c344474 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -13,7 +13,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.http.HttpStatus; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.proto.FetchResult; diff --git a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java index 791a1da06c1..268de61b246 100755 --- a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java @@ -2,7 +2,7 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java index 72ec6feb2b2..30d78ea27c0 100644 --- a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java @@ -1,7 +1,7 @@ package org.prebid.server.geolocation; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.geolocation.model.GeoInfoConfiguration; diff --git a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java index 7604a25c71a..3d4c582db38 100644 --- a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java @@ -1,7 +1,7 @@ package org.prebid.server.geolocation; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; /** diff --git a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java index 2cea7119714..5afa9311cba 100644 --- a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java @@ -14,8 +14,8 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.execution.RemoteFileProcessor; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import java.io.FileInputStream; @@ -28,7 +28,7 @@ * Implementation of the {@link GeoLocationService} * backed by MaxMind free database */ -public class MaxMindGeoLocationService implements GeoLocationService, RemoteFileProcessor { +public class MaxMindGeoLocationService implements GeoLocationService, FileProcessor { private static final String VENDOR = "maxmind"; diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java index 746dffb5fc7..3ac0f44069c 100644 --- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java +++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java @@ -24,8 +24,8 @@ import org.prebid.server.cookie.model.CookieSyncContext; import org.prebid.server.cookie.model.PartitionedCookie; import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; diff --git a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java index 971cb82204f..60e11195c26 100644 --- a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java +++ b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java @@ -18,7 +18,7 @@ import org.prebid.server.events.EventRequest; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.model.Endpoint; diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index c036bb310cd..4425e698458 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -37,8 +37,8 @@ import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/VtrackHandler.java index 6539881eaa4..3d1243264d9 100644 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ b/src/main/java/org/prebid/server/handler/VtrackHandler.java @@ -17,8 +17,8 @@ import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.events.EventUtil; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.DecodeException; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; diff --git a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java index 6243f9ed8c7..97bd43c4abf 100644 --- a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java +++ b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java @@ -1,7 +1,7 @@ package org.prebid.server.health; import io.vertx.core.Vertx; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.model.Status; import org.prebid.server.health.model.StatusResponse; diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java index ce5a8df813a..8e565d29d90 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -11,8 +11,8 @@ import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java index 6ed23ef8980..99399d5ba6b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java @@ -2,7 +2,7 @@ import lombok.Value; import lombok.experimental.Accessors; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.model.Endpoint; diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java index 7c3b6c922d3..22493ea8a07 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java @@ -1,6 +1,6 @@ package org.prebid.server.hooks.v1; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.model.Endpoint; public interface InvocationContext { diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java index b02698bbce3..5c994ec590f 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java @@ -10,7 +10,7 @@ import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index da414bef279..7a6582ccd42 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -1,7 +1,7 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 97348e7bbd8..9f8fcea9ff2 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -3,7 +3,7 @@ import io.vertx.core.Future; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; diff --git a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java index 2edd16b7345..32d47d6abad 100644 --- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java @@ -1,7 +1,7 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java index 9dad7f6a28b..c346e4824f4 100644 --- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -7,7 +7,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.helper.DatabaseStoredDataResultMapper; diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 11a0d2cb3af..bfde0fc2e81 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -4,7 +4,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.ActivitiesConfigResolver; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index 33a1ea36390..1a2f42e86c4 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -8,7 +8,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 1c78e4693c5..98517003baf 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -9,7 +9,7 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.Logger; diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java index f6198a5ad94..f1c8b107c5f 100644 --- a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -8,7 +8,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.Tuple2; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java index cc4b80ad870..bdd9e8258e0 100644 --- a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java +++ b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java @@ -4,8 +4,8 @@ import io.vertx.core.Promise; import io.vertx.core.Vertx; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.MetricName; diff --git a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java index 7edc69d6176..bfb56b8c0c1 100644 --- a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java @@ -1,16 +1,12 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpClientOptions; import lombok.Data; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.GeoLocationServiceWrapper; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; -import org.prebid.server.execution.RemoteFileSyncer; -import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; -import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; -import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.execution.file.syncer.FileSyncer; import org.prebid.server.geolocation.CircuitBreakerSecuredGeoLocationService; import org.prebid.server.geolocation.ConfigurationGeoLocationService; import org.prebid.server.geolocation.CountryCodeMapper; @@ -18,9 +14,7 @@ import org.prebid.server.geolocation.MaxMindGeoLocationService; import org.prebid.server.metric.Metrics; import org.prebid.server.spring.config.model.CircuitBreakerProperties; -import org.prebid.server.spring.config.model.ExponentialBackoffProperties; -import org.prebid.server.spring.config.model.HttpClientProperties; -import org.prebid.server.spring.config.model.RemoteFileSyncerProperties; +import org.prebid.server.spring.config.model.FileSyncerProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -57,14 +51,14 @@ CircuitBreakerProperties maxMindCircuitBreakerProperties() { @Bean @ConfigurationProperties(prefix = "geolocation.maxmind.remote-file-syncer") - RemoteFileSyncerProperties maxMindRemoteFileSyncerProperties() { - return new RemoteFileSyncerProperties(); + FileSyncerProperties maxMindRemoteFileSyncerProperties() { + return new FileSyncerProperties(); } @Bean @ConditionalOnProperty(prefix = "geolocation.circuit-breaker", name = "enabled", havingValue = "false", matchIfMissing = true) - GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncerProperties, + GeoLocationService basicGeoLocationService(FileSyncerProperties fileSyncerProperties, Vertx vertx) { return createGeoLocationService(fileSyncerProperties, vertx); @@ -75,7 +69,7 @@ GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncer CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( Vertx vertx, Metrics metrics, - RemoteFileSyncerProperties fileSyncerProperties, + FileSyncerProperties fileSyncerProperties, @Qualifier("maxMindCircuitBreakerProperties") CircuitBreakerProperties circuitBreakerProperties, Clock clock) { @@ -85,49 +79,12 @@ CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( circuitBreakerProperties.getClosingIntervalMs(), clock); } - private GeoLocationService createGeoLocationService(RemoteFileSyncerProperties properties, Vertx vertx) { + private GeoLocationService createGeoLocationService(FileSyncerProperties properties, Vertx vertx) { final MaxMindGeoLocationService maxMindGeoLocationService = new MaxMindGeoLocationService(); - final HttpClientProperties httpClientProperties = properties.getHttpClient(); - final HttpClientOptions httpClientOptions = new HttpClientOptions() - .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) - .setMaxRedirects(httpClientProperties.getMaxRedirects()); - - final RemoteFileSyncer remoteFileSyncer = new RemoteFileSyncer( - maxMindGeoLocationService, - properties.getDownloadUrl(), - properties.getSaveFilepath(), - properties.getTmpFilepath(), - toRetryPolicy(properties), - properties.getTimeoutMs(), - properties.getUpdateIntervalMs(), - vertx.createHttpClient(httpClientOptions), - vertx); - - remoteFileSyncer.sync(); + final FileSyncer fileSyncer = FileUtil.fileSyncerFor(maxMindGeoLocationService, properties, vertx); + fileSyncer.sync(); return maxMindGeoLocationService; } - - // TODO: remove after transition period - private static RetryPolicy toRetryPolicy(RemoteFileSyncerProperties properties) { - final Long retryIntervalMs = properties.getRetryIntervalMs(); - final Integer retryCount = properties.getRetryCount(); - final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount); - final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount) - || !fixedRetryPolicyDefined; - - if (!fixedRetryPolicyValid) { - throw new IllegalArgumentException("fixed interval retry policy is invalid"); - } - - final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry(); - return fixedRetryPolicyDefined - ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount) - : ExponentialBackoffRetryPolicy.of( - exponentialBackoffProperties.getDelayMillis(), - exponentialBackoffProperties.getMaxDelayMillis(), - exponentialBackoffProperties.getFactor(), - exponentialBackoffProperties.getJitter()); - } } @Configuration diff --git a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java index 93eaeb48030..836b45ca285 100644 --- a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java @@ -2,7 +2,7 @@ import io.vertx.core.Vertx; import io.vertx.sqlclient.Pool; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.ApplicationChecker; import org.prebid.server.health.DatabaseHealthChecker; diff --git a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java index 6b77be58153..64534d119aa 100644 --- a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java @@ -3,7 +3,7 @@ import io.vertx.core.Vertx; import lombok.Data; import lombok.NoArgsConstructor; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.HookCatalog; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.v1.Module; diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java index 79a62ffbe87..8a483e92a4d 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -3,7 +3,7 @@ import io.vertx.core.Vertx; import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.BasicPriceFloorAdjuster; import org.prebid.server.floors.BasicPriceFloorEnforcer; import org.prebid.server.floors.BasicPriceFloorProcessor; diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index c2ee2ca5dfa..778351b95a9 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -87,7 +87,7 @@ import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.events.EventsService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index f7aaa9bb4ba..f101495eb66 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -7,7 +7,7 @@ import lombok.experimental.UtilityClass; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.activity.ActivitiesConfigResolver; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; diff --git a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java similarity index 83% rename from src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java rename to src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java index 09e56ac59c6..54dbd81a5a9 100644 --- a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java +++ b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java @@ -11,7 +11,9 @@ @Validated @Data @NoArgsConstructor -public class RemoteFileSyncerProperties { +public class FileSyncerProperties { + + private Type type = Type.REMOTE; @NotBlank private String downloadUrl; @@ -37,6 +39,13 @@ public class RemoteFileSyncerProperties { @NotNull private Long updateIntervalMs; + private boolean checkSize; + @NotNull private HttpClientProperties httpClient; + + public enum Type { + + LOCAL, REMOTE + } } diff --git a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java index 82794d58ffb..8c93941c679 100644 --- a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java @@ -29,7 +29,7 @@ import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.cookie.CookieSyncService; import org.prebid.server.cookie.UidsCookieService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.handler.BidderParamHandler; import org.prebid.server.handler.CookieSyncHandler; import org.prebid.server.handler.ExceptionHandler; diff --git a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java index e5aa90aabb2..7158bd4ba07 100644 --- a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java +++ b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java @@ -6,7 +6,7 @@ import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.Tuple; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.metric.Metrics; diff --git a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java index bb4fa7d09c1..ea59c9f9670 100644 --- a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java +++ b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java @@ -4,7 +4,7 @@ import io.vertx.core.Vertx; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; diff --git a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java index 87c9ada84c6..78a6a34ac7e 100644 --- a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java +++ b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java @@ -3,7 +3,7 @@ import io.vertx.core.Future; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import java.util.List; import java.util.function.Function; diff --git a/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java index 4fc744e4b43..b7418e7b777 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java @@ -16,7 +16,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import org.springframework.test.util.ReflectionTestUtils; diff --git a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java index d1de6726c42..3c05d00af37 100644 --- a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java +++ b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java @@ -18,8 +18,8 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 17de10e38b5..0e3d0220ba1 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -62,8 +62,8 @@ import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 60cc83291f7..e8d78c33b1a 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -70,8 +70,8 @@ import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; diff --git a/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java b/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java index 12fed4e7f19..be7348abb74 100644 --- a/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java +++ b/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java @@ -15,8 +15,8 @@ import org.prebid.server.auction.model.IpAddress.IP; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java index adcd1e9a296..3976a4bbd8f 100644 --- a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java +++ b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java @@ -13,7 +13,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.StoredResponseResult; import org.prebid.server.auction.model.TimeoutContext; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; diff --git a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java index 8cbd5bc8b70..9b9aa5aba48 100644 --- a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java @@ -19,7 +19,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java index 3b625ddf379..a137578ef14 100644 --- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java @@ -23,8 +23,8 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.proto.openrtb.ext.request.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse; diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java index 3946162ff92..66442d47c93 100644 --- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java @@ -24,7 +24,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.json.JsonMerger; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; diff --git a/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java b/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java index fd005459913..477e67ffd0b 100644 --- a/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java @@ -14,7 +14,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java b/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java index 453210aabd1..1cf5961ff4e 100644 --- a/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java @@ -14,7 +14,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java index b38c305cd2b..5b4c0bd72e5 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java @@ -39,8 +39,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java index 2970e643595..81180793e22 100644 --- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java +++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java @@ -33,8 +33,8 @@ 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.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index a8ce602aeb5..058f958fa48 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -32,8 +32,8 @@ import org.prebid.server.events.EventsContext; import org.prebid.server.events.EventsService; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.UUIDIdGenerator; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java b/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java new file mode 100644 index 00000000000..407e01c6bf5 --- /dev/null +++ b/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java @@ -0,0 +1,68 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +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.assertion.FutureAssertion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class LocalFileSupplierTest { + + @Mock + private FileSystem fileSystem; + + private LocalFileSupplier target; + + @BeforeEach + public void setUp() { + given(fileSystem.exists(anyString())).willReturn(Future.succeededFuture(true)); + + target = new LocalFileSupplier("/path/to/file", fileSystem); + } + + @Test + public void getShouldReturnFailedFutureIfFileNotFound() { + // given + given(fileSystem.exists(anyString())).willReturn(Future.succeededFuture(false)); + + // when and then + FutureAssertion.assertThat(target.get()).isFailed().hasMessage("File /path/to/file not found."); + } + + @Test + public void getShouldReturnFilePath() { + // given + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.props(anyString())).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.creationTime()).willReturn(1000L); + + // when and then + assertThat(target.get().result()).isEqualTo("/path/to/file"); + } + + @Test + public void getShouldReturnNullIfFileNotModifiedSinceLastTry() { + // given + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.props(anyString())).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.creationTime()).willReturn(1000L); + + // when + target.get(); + final Future result = target.get(); + + // then + assertThat(result.succeeded()).isTrue(); + assertThat(result.result()).isNull(); + } +} diff --git a/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java b/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java new file mode 100644 index 00000000000..d60ff696ba7 --- /dev/null +++ b/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java @@ -0,0 +1,244 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.file.AsyncFile; +import io.vertx.core.file.CopyOptions; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +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.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.assertion.FutureAssertion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class RemoteFileSupplierTest { + + private static final String SAVE_PATH = "/path/to/file"; + private static final String BACKUP_PATH = SAVE_PATH + ".old"; + private static final String TMP_PATH = "/path/to/tmp"; + + @Mock + private HttpClient httpClient; + + @Mock + private FileSystem fileSystem; + + private RemoteFileSupplier target; + + @Mock + private HttpClientResponse getResponse; + + @Mock + private HttpClientResponse headResponse; + + @BeforeEach + public void setUp() { + final HttpClientRequest getRequest = mock(HttpClientRequest.class); + given(httpClient.request(argThat(requestOptions -> + requestOptions != null && requestOptions.getMethod().equals(HttpMethod.GET)))) + .willReturn(Future.succeededFuture(getRequest)); + given(getRequest.send()).willReturn(Future.succeededFuture(getResponse)); + + final HttpClientRequest headRequest = mock(HttpClientRequest.class); + given(httpClient.request(argThat(requestOptions -> + requestOptions != null && requestOptions.getMethod().equals(HttpMethod.HEAD)))) + .willReturn(Future.succeededFuture(headRequest)); + given(headRequest.send()).willReturn(Future.succeededFuture(headResponse)); + given(headResponse.statusCode()).willReturn(200); + + target = target(false); + } + + private RemoteFileSupplier target(boolean checkRemoteFileSize) { + return new RemoteFileSupplier( + "https://download.url/", + SAVE_PATH, + TMP_PATH, + httpClient, + 1000L, + checkRemoteFileSize, + fileSystem); + } + + @Test + public void shouldCheckWritePermissionsForFiles() { + // given + reset(fileSystem); + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.existsBlocking(anyString())).willReturn(true); + given(fileSystem.propsBlocking(anyString())).willReturn(fileProps); + given(fileProps.isDirectory()).willReturn(false); + + // when + target(false); + + // then + verify(fileSystem, times(3)).mkdirsBlocking(anyString()); + } + + @Test + public void getShouldReturnFailureWhenCanNotOpenTmpFile() { + // given + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.failedFuture("Failure.")); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Promise.promise().future()); + + // when + final Future result = target.get(); + + // then + FutureAssertion.assertThat(result).isFailed().hasMessage("Failure."); + } + + @Test + public void getShouldReturnFailureOnNotOkStatusCode() { + // given + final AsyncFile tmpFile = mock(AsyncFile.class); + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile)); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Promise.promise().future()); + + given(getResponse.statusCode()).willReturn(204); + + // when + final Future result = target.get(); + + // then + FutureAssertion.assertThat(result).isFailed() + .hasMessage("Got unexpected response from server with status code 204 and message null"); + } + + @Test + public void getShouldReturnExpectedResult() { + // given + final AsyncFile tmpFile = mock(AsyncFile.class); + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile)); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.move(eq(SAVE_PATH), eq(BACKUP_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + given(fileSystem.move(eq(TMP_PATH), eq(SAVE_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + + given(getResponse.statusCode()).willReturn(200); + given(getResponse.pipeTo(any())).willReturn(Future.succeededFuture()); + + // when + final Future result = target.get(); + + // then + verify(tmpFile).close(); + assertThat(result.result()).isEqualTo(SAVE_PATH); + } + + @Test + public void getShouldReturnExpectedResultWhenCheckRemoteFileSizeIsTrue() { + // given + target = target(true); + + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.props(eq(SAVE_PATH))).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.size()).willReturn(1000L); + + given(headResponse.statusCode()).willReturn(200); + given(headResponse.getHeader(eq(HttpHeaders.CONTENT_LENGTH))).willReturn("1001"); + + final AsyncFile tmpFile = mock(AsyncFile.class); + given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile)); + given(fileSystem.move(eq(SAVE_PATH), eq(BACKUP_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + given(fileSystem.move(eq(TMP_PATH), eq(SAVE_PATH), Mockito.any())) + .willReturn(Future.succeededFuture()); + + given(getResponse.statusCode()).willReturn(200); + given(getResponse.pipeTo(any())).willReturn(Future.succeededFuture()); + + // when + final Future result = target.get(); + + // then + verify(tmpFile).close(); + assertThat(result.result()).isEqualTo(SAVE_PATH); + } + + @Test + public void getShouldReturnNullWhenCheckRemoteFileSizeIsTrueAndSizeNotChanged() { + // given + target = target(true); + + final FileProps fileProps = mock(FileProps.class); + given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.props(eq(SAVE_PATH))).willReturn(Future.succeededFuture(fileProps)); + given(fileProps.size()).willReturn(1000L); + + given(headResponse.statusCode()).willReturn(200); + given(headResponse.getHeader(eq(HttpHeaders.CONTENT_LENGTH))).willReturn("1000"); + + // when + final Future result = target.get(); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void clearTmpShouldCallExpectedMethods() { + // given + given(fileSystem.exists(eq(TMP_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.delete(eq(TMP_PATH))).willReturn(Future.succeededFuture()); + + // when + target.clearTmp(); + + // then + verify(fileSystem).delete(TMP_PATH); + } + + @Test + public void deleteBackupShouldCallExpectedMethods() { + // given + given(fileSystem.exists(eq(BACKUP_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.delete(eq(BACKUP_PATH))).willReturn(Future.succeededFuture()); + + // when + target.deleteBackup(); + + // then + verify(fileSystem).delete(BACKUP_PATH); + } + + @Test + public void restoreFromBackupShouldCallExpectedMethods() { + // given + given(fileSystem.exists(eq(BACKUP_PATH))).willReturn(Future.succeededFuture(true)); + given(fileSystem.move(eq(BACKUP_PATH), eq(SAVE_PATH))).willReturn(Future.succeededFuture()); + given(fileSystem.delete(eq(BACKUP_PATH))).willReturn(Future.succeededFuture()); + + // when + target.deleteBackup(); + + // then + verify(fileSystem).delete(BACKUP_PATH); + } +} diff --git a/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java b/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java new file mode 100644 index 00000000000..39a8e8ae966 --- /dev/null +++ b/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java @@ -0,0 +1,160 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +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.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; +import org.prebid.server.execution.retry.NonRetryable; +import org.prebid.server.execution.retry.RetryPolicy; +import org.testcontainers.shaded.org.apache.commons.lang3.NotImplementedException; + +import java.util.concurrent.Callable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class FileSyncerTest { + + private static final String SAVE_PATH = "/path/to/file"; + + @Mock + private FileProcessor fileProcessor; + + @Mock + private Vertx vertx; + + @BeforeEach + public void setUp() { + given(vertx.executeBlocking(Mockito.>any())).willAnswer(invocation -> { + try { + return Future.succeededFuture(((Callable) invocation.getArgument(0)).call()); + } catch (Throwable e) { + return Future.failedFuture(e); + } + }); + } + + @Test + public void syncShouldCallExpectedMethodsOnSuccessWhenNoReturnedFile() { + // given + final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance()); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture()); + + // when + fileSyncer.sync(); + + // then + verifyNoInteractions(fileProcessor); + verify(fileSyncer).doOnSuccess(); + verify(vertx).setTimer(eq(1000L), any()); + } + + @Test + public void syncShouldCallExpectedMethodsOnSuccess() { + // given + final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance()); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH)); + given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.succeededFuture()); + + // when + fileSyncer.sync(); + + // then + verify(fileProcessor).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer).doOnSuccess(); + verify(vertx).setTimer(eq(1000L), any()); + } + + @Test + public void syncShouldCallExpectedMethodsOnFailure() { + // given + final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance()); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH)); + given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.failedFuture("Failure")); + + // when + fileSyncer.sync(); + + // then + verify(fileProcessor).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer).doOnFailure(any()); + verify(vertx).setTimer(eq(1000L), any()); + } + + @Test + public void syncShouldCallExpectedMethodsOnFailureWithRetryable() { + // given + final FileSyncer fileSyncer = fileSyncer(FixedIntervalRetryPolicy.limited(10L, 1)); + given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH)); + given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.failedFuture("Failure")); + + final Promise promise = Promise.promise(); + given(vertx.setTimer(eq(10L), any())).willAnswer(invocation -> { + promise.future().onComplete(ignore -> ((Handler) invocation.getArgument(1)).handle(1L)); + return 1L; + }); + + // when + fileSyncer.sync(); + + // then + verify(fileProcessor).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer).doOnFailure(any()); + verify(vertx).setTimer(eq(10L), any()); + + // when + promise.complete(); + + // then + verify(fileProcessor, times(2)).setDataPath(eq(SAVE_PATH)); + verify(fileSyncer, times(2)).doOnFailure(any()); + verify(vertx).setTimer(eq(1000L), any()); + } + + private FileSyncer fileSyncer(RetryPolicy retryPolicy) { + return spy(new TestFileSyncer(fileProcessor, 1000L, retryPolicy, vertx)); + } + + private static class TestFileSyncer extends FileSyncer { + + protected TestFileSyncer(FileProcessor fileProcessor, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + } + + @Override + public Future getFile() { + return Future.failedFuture(new NotImplementedException()); + } + + @Override + protected Future doOnSuccess() { + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + return Future.succeededFuture(); + } + } +} diff --git a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java b/src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java similarity index 87% rename from src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java rename to src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java index 9acd719d30f..2da614c4e50 100644 --- a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java +++ b/src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.file.syncer; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; @@ -17,14 +17,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileProcessor; import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; import org.prebid.server.execution.retry.RetryPolicy; import java.io.File; +import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -57,7 +60,8 @@ public class RemoteFileSyncerTest extends VertxTest { private static final String TMP_FILE_PATH = String.join(File.separator, "tmp", "fake", "path", "to", "file.pdf"); private static final String DIR_PATH = String.join(File.separator, "fake", "path", "to"); private static final Long FILE_SIZE = 2131242L; - @Mock + + @Mock(strictness = LENIENT) private Vertx vertx; @Mock(strictness = LENIENT) @@ -67,7 +71,7 @@ public class RemoteFileSyncerTest extends VertxTest { private HttpClient httpClient; @Mock(strictness = LENIENT) - private RemoteFileProcessor remoteFileProcessor; + private FileProcessor fileProcessor; @Mock private AsyncFile asyncFile; @@ -85,30 +89,38 @@ public class RemoteFileSyncerTest extends VertxTest { @BeforeEach public void setUp() { when(vertx.fileSystem()).thenReturn(fileSystem); - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + given(vertx.executeBlocking(Mockito.>any())).willAnswer(invocation -> { + try { + return Future.succeededFuture(((Callable) invocation.getArgument(0)).call()); + } catch (Throwable e) { + return Future.failedFuture(e); + } + }); + + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, 0, httpClient, vertx); } @Test public void shouldThrowNullPointerExceptionWhenIllegalArgumentsWhenNullArguments() { assertThatNullPointerException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, null, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, + () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, null, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)); assertThatNullPointerException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, null, vertx)); assertThatNullPointerException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, null)); } @Test public void shouldThrowIllegalArgumentExceptionWhenIllegalArguments() { assertThatIllegalArgumentException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, null, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, null, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)); assertThatIllegalArgumentException().isThrownBy( - () -> new RemoteFileSyncer(remoteFileProcessor, "bad url", FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + () -> new RemoteFileSyncer(fileProcessor, "bad url", FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)); } @@ -119,7 +131,7 @@ public void creteShouldCreateDirWithWritePermissionIfDirNotExist() { when(fileSystem.existsBlocking(eq(DIR_PATH))).thenReturn(false); // when - new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, + new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); // then @@ -136,7 +148,7 @@ public void createShouldCreateDirWithWritePermissionIfItsNotDir() { when(fileProps.isDirectory()).thenReturn(false); // when - new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, + new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); // then @@ -151,7 +163,7 @@ public void createShouldThrowPreBidExceptionWhenPropsThrowException() { when(fileSystem.propsBlocking(eq(DIR_PATH))).thenThrow(FileSystemException.class); // when and then - assertThatThrownBy(() -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, + assertThatThrownBy(() -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx)) .isInstanceOf(PreBidException.class); } @@ -167,14 +179,14 @@ public void syncForFilepathShouldNotTriggerServiceWhenCantCheckIfUsableFileExist // then verify(fileSystem).exists(eq(FILE_PATH)); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); verifyNoInteractions(httpClient); } @Test public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { // given - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -194,7 +206,7 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { // then verify(fileSystem, times(2)).exists(eq(FILE_PATH)); verify(httpClient).request(any()); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(), any()); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -203,7 +215,7 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() { @Test public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { // given - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -228,7 +240,7 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { verify(httpClient).request(any()); verify(httpClientResponse).getHeader(eq(HttpHeaders.CONTENT_LENGTH)); verify(fileSystem).props(eq(FILE_PATH)); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -237,7 +249,7 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() { @Test public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { // given - remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -264,7 +276,7 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { verify(httpClient).request(any()); verify(httpClientResponse).getHeader(eq(HttpHeaders.CONTENT_LENGTH)); verify(fileSystem).props(eq(FILE_PATH)); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -274,7 +286,7 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() { public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { // given remoteFileSyncer = new RemoteFileSyncer( - remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -304,7 +316,7 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { given(fileSystem.move(anyString(), any(), any(CopyOptions.class))) .willReturn(Future.succeededFuture()); - given(remoteFileProcessor.setDataPath(anyString())) + given(fileProcessor.setDataPath(anyString())) .willReturn(Future.succeededFuture()); // when @@ -319,7 +331,7 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() { verify(fileSystem).open(eq(TMP_FILE_PATH), any()); verify(asyncFile).close(); - verify(remoteFileProcessor, times(2)).setDataPath(any()); + verify(fileProcessor, times(2)).setDataPath(any()); verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verifyNoMoreInteractions(httpClient); @@ -347,7 +359,7 @@ public void syncForFilepathShouldRetryAfterFailedDownload() { verify(fileSystem, times(RETRY_COUNT + 1)).open(eq(TMP_FILE_PATH), any()); verifyNoInteractions(httpClient); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); } @Test @@ -368,7 +380,7 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture())) .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture(new RuntimeException()))); - given(remoteFileProcessor.setDataPath(anyString())) + given(fileProcessor.setDataPath(anyString())) .willReturn(Future.succeededFuture()); // when @@ -379,13 +391,13 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() { verify(fileSystem, times(RETRY_COUNT + 1)).delete(eq(TMP_FILE_PATH)); verifyNoInteractions(httpClient); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); } @Test public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotSet() { // given - given(remoteFileProcessor.setDataPath(anyString())) + given(fileProcessor.setDataPath(anyString())) .willReturn(Future.succeededFuture()); given(fileSystem.exists(anyString())) @@ -414,7 +426,7 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS verify(httpClient).request(any()); verify(asyncFile).close(); verify(httpClientResponse).statusCode(); - verify(remoteFileProcessor).setDataPath(any()); + verify(fileProcessor).setDataPath(any()); verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class)); verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); verifyNoMoreInteractions(httpClient); @@ -452,7 +464,7 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() { verify(httpClient, times(RETRY_COUNT + 1)).request(any()); verify(asyncFile, times(RETRY_COUNT + 1)).close(); - verifyNoInteractions(remoteFileProcessor); + verifyNoInteractions(fileProcessor); } @Test @@ -484,7 +496,7 @@ public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() { verify(httpClient).request(any()); verify(httpClientResponse).statusCode(); verify(httpClientResponse, never()).pipeTo(any()); - verify(remoteFileProcessor, never()).setDataPath(any()); + verify(fileProcessor, never()).setDataPath(any()); verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any()); } @@ -492,7 +504,7 @@ public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() { public void syncShouldNotUpdateFileWhenServerRespondsWithNonOkStatusCode() { // given remoteFileSyncer = new RemoteFileSyncer( - remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, + fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx); givenTriggerUpdate(); @@ -528,7 +540,7 @@ private void givenTriggerUpdate() { given(fileSystem.exists(anyString())) .willReturn(Future.succeededFuture(true)); - given(remoteFileProcessor.setDataPath(anyString())) + given(fileProcessor.setDataPath(anyString())) .willReturn(Future.succeededFuture()); given(vertx.setPeriodic(eq(UPDATE_INTERVAL), any())) diff --git a/src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java b/src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java similarity index 97% rename from src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java rename to src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java index e38e4c7304d..9e4a140cb7b 100644 --- a/src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java +++ b/src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/prebid/server/execution/TimeoutTest.java b/src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java similarity index 95% rename from src/test/java/org/prebid/server/execution/TimeoutTest.java rename to src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java index c45589bc86a..c1c7dfd7ca3 100644 --- a/src/test/java/org/prebid/server/execution/TimeoutTest.java +++ b/src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java index 59253e200a7..64a66c507c0 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java @@ -12,7 +12,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.model.PriceFloorField; diff --git a/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java b/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java index 945d292970f..05643ce0e57 100644 --- a/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java +++ b/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.geolocation.model.GeoInfoConfiguration; diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java index e5c71464d05..0b6027fcc92 100644 --- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java @@ -32,7 +32,7 @@ import org.prebid.server.cookie.model.PartitionedCookie; import org.prebid.server.cookie.proto.Uids; import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.ccpa.Ccpa; import org.prebid.server.privacy.gdpr.model.TcfContext; diff --git a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java index 9f27a7dde1f..c386ca753ef 100644 --- a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java @@ -19,7 +19,7 @@ import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.model.Tuple2; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.HttpRequestContext; import org.prebid.server.settings.ApplicationSettings; diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java index 5df909ebd78..95e5ad24ced 100644 --- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java @@ -33,7 +33,7 @@ import org.prebid.server.cookie.proto.Uids; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.privacy.HostVendorTcfDefinerService; import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; diff --git a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java index e0f2e5a966c..0674ab1d1ce 100644 --- a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java @@ -21,7 +21,7 @@ import org.prebid.server.cache.proto.response.bid.BidCacheResponse; import org.prebid.server.cache.proto.response.bid.CacheObject; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java index 9f40ed1bc81..e22a9178e84 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java @@ -39,8 +39,8 @@ import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java index c2a84bcc922..7ff40d09899 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java @@ -31,8 +31,8 @@ import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; diff --git a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java index fe59fb31fcc..5da93cd4c94 100644 --- a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java +++ b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java @@ -29,8 +29,8 @@ import org.prebid.server.cookie.UidsCookie; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.UnauthorizedAccountException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import org.prebid.server.proto.response.VideoResponse; import org.prebid.server.settings.model.Account; diff --git a/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java b/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java index e7b8bff269e..b46abafac7e 100644 --- a/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java +++ b/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java @@ -9,7 +9,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.health.model.StatusResponse; diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java index 8b2358b9708..22d8e49f6a2 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -28,7 +28,7 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionAction; import org.prebid.server.hooks.execution.model.ExecutionGroup; diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java index d09df3327a8..1767491959a 100644 --- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java @@ -8,8 +8,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.model.Account; diff --git a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java index bab03ea0bb8..86c72604150 100644 --- a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java @@ -8,8 +8,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.helper.ParametrizedQueryHelper; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java index aaa72b45756..f3180d3651e 100644 --- a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -8,7 +8,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.activity.ActivitiesConfigResolver; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.floors.PriceFloorsConfigResolver; import org.prebid.server.json.JsonMerger; import org.prebid.server.settings.model.Account; diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java index b2452e06cae..e3076ddbdfd 100644 --- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java @@ -10,8 +10,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountPrivacyConfig; diff --git a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java index 2f7c293f9f8..a702d71ab2e 100644 --- a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java @@ -13,7 +13,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; diff --git a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java index d0010e7699c..1e1ffd37271 100644 --- a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java +++ b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java @@ -10,7 +10,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.MetricName; import org.prebid.server.metric.Metrics; import org.prebid.server.settings.CacheNotificationListener; diff --git a/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java b/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java index e182255df87..15c76f3b6c7 100644 --- a/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java +++ b/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java @@ -14,8 +14,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import java.time.Clock; diff --git a/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java b/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java index fe6bc6c337b..abc2d8af479 100644 --- a/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java +++ b/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java @@ -14,8 +14,8 @@ import org.mockito.BDDMockito; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.metric.Metrics; import java.time.Clock; From 4286f77195e46a5ced8be0dd0e001965b1ae227e Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:23:53 -0500 Subject: [PATCH 129/170] Unruly: Remove GZIP compression as this breaks the bidder (#3553) --- src/main/resources/bidder-config/unruly.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml index af4f8690282..0b239b0a3f7 100644 --- a/src/main/resources/bidder-config/unruly.yaml +++ b/src/main/resources/bidder-config/unruly.yaml @@ -2,7 +2,6 @@ adapters: unruly: endpoint: https://targeting.unrulymedia.com/unruly_prebid_server ortb-version: "2.6" - endpoint-compression: gzip meta-info: maintainer-email: prebidsupport@unrulygroup.com app-media-types: From f039717f815b3924adc14e746ab8ac96cfcc7a1d Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:27:01 +0100 Subject: [PATCH 130/170] 51Degree: Remove Redundant Logback Dependency (#3552) --- extra/modules/fiftyone-devicedetection/pom.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index b4a290dd118..16a60f7bed8 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -15,7 +15,6 @@ 4.4.94 - 1.2.13 @@ -32,18 +31,5 @@ device-detection ${fiftyone-device-detection.version} - - - ch.qos.logback - logback-classic - ${logback.version} - test - - - ch.qos.logback - logback-core - ${logback.version} - test - From 0aa35c1a056de1f085ef7200d7cb69e4fecd9e3a Mon Sep 17 00:00:00 2001 From: Muki Seiler Date: Thu, 21 Nov 2024 10:48:07 +0100 Subject: [PATCH 131/170] Add code highlighting (#3557) --- docs/developers/code-style.md | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/developers/code-style.md b/docs/developers/code-style.md index 14704d20799..de42811030f 100644 --- a/docs/developers/code-style.md +++ b/docs/developers/code-style.md @@ -28,7 +28,7 @@ in `pom.xml` directly. It is recommended to define version of library to separate property in `pom.xml`: -``` +```xml 2.6.2 @@ -48,7 +48,7 @@ It is recommended to define version of library to separate property in `pom.xml` Do not use wildcard in imports because they hide what exactly is required by the class. -``` +```java // bad import java.util.*; @@ -61,7 +61,7 @@ import java.util.Map; Prefer to use `camelCase` naming convention for variables and methods. -``` +```java // bad String account_id = "id"; @@ -71,7 +71,7 @@ String accountId = "id"; Name of variable should be self-explanatory: -``` +```java // bad String s = resolveParamA(); @@ -83,7 +83,7 @@ This helps other developers flesh your code out better without additional questi For `Map`s it is recommended to use `To` between key and value designation: -``` +```java // bad Map map = getData(); @@ -97,7 +97,7 @@ Make data transfer object(DTO) classes immutable with static constructor. This can be achieved by using Lombok and `@Value(staticConstructor="of")`. When constructor uses multiple(more than 4) arguments, use builder instead(`@Builder`). If dto must be modified somewhere, use builders annotation `toBuilder=true` parameter and rebuild instance by calling `toBuilder()` method. -``` +```java // bad public class MyDto { @@ -138,7 +138,7 @@ final MyDto updatedDto = myDto.toBuilder().value("newValue").build(); Although Java supports the `var` keyword at the time of writing this documentation, the maintainers have chosen not to utilize it within the PBS codebase. Instead, write full variable type. -``` +```java // bad final var result = getResult(); @@ -150,7 +150,7 @@ final Data result = getResult(); Enclosing parenthesis should be placed on expression end. -``` +```java // bad methodCall( long list of arguments @@ -163,7 +163,7 @@ methodCall( This also applies for nested expressions. -``` +```java // bad methodCall( nestedCall( @@ -181,7 +181,7 @@ methodCall( Please, place methods inside a class in call order. -``` +```java // bad public interface Test { @@ -249,7 +249,7 @@ Define interface first method, then all methods that it is calling, then second Not strict, but methods with long parameters list, that cannot be placed on single line, should add empty line before body definition. -``` +```java // bad public static void method( parameters definitions) { @@ -266,7 +266,7 @@ public static void method( Use collection literals where it is possible to define and initialize collections. -``` +```java // bad final List foo = new ArrayList(); foo.add("foo"); @@ -278,7 +278,7 @@ final List foo = List.of("foo", "bar"); Also, use special methods of Collections class for empty or single-value one-line collection creation. This makes developer intention clear and code less error-prone. -``` +```java // bad return List.of(); @@ -296,7 +296,7 @@ return Collections.singletonList("foo"); It is recommended to declare variable as `final`- not strict but rather project convention to keep the code safe. -``` +```java // bad String value = "value"; @@ -308,7 +308,7 @@ final String value = "value"; Results of long ternary operators should be on separate lines: -``` +```java // bad boolean result = someVeryVeryLongConditionThatForcesLineWrap ? firstResult : secondResult; @@ -321,7 +321,7 @@ boolean result = someVeryVeryLongConditionThatForcesLineWrap Not so strict, but short ternary operations should be on one line: -``` +```java // bad boolean result = someShortCondition ? firstResult @@ -335,7 +335,7 @@ boolean result = someShortCondition ? firstResult : secondResult; Do not rely on operator precedence in boolean logic, use parenthesis instead. This will make code simpler and less error-prone. -``` +```java // bad final boolean result = a && b || c; @@ -347,7 +347,7 @@ final boolean result = (a && b) || c; Try to avoid hard-readable multiple nested method calls: -``` +```java // bad int resolvedValue = resolveValue(fetchExternalJson(url, httpClient), populateAdditionalKeys(mainKeys, keyResolver)); @@ -361,7 +361,7 @@ int resolvedValue = resolveValue(externalJson, additionalKeys); Try not to retrieve same data more than once: -``` +```java // bad if (getData() != null) { final Data resolvedData = resolveData(getData()); @@ -380,7 +380,7 @@ if (data != null) { If you're dealing with incoming data, please be sure to check if the nested object is not null before chaining. -``` +```java // bad final ExtRequestTargeting targeting = bidRequest.getExt().getPrebid().getTargeting(); @@ -400,7 +400,7 @@ We are trying to get rid of long chains of null checks, which are described in s Don't leave commented code (don't think about the future). -``` +```java // bad // String iWillUseThisLater = "never"; ``` @@ -426,7 +426,7 @@ The code should be covered over 90%. The common way for writing tests has to comply with `given-when-then` style. -``` +```java // given final BidRequest bidRequest = BidRequest.builder().id("").build(); @@ -451,7 +451,7 @@ The team decided to use name `target` for class instance under test. Unit tests should be as granular as possible. Try to split unit tests into smaller ones until this is impossible to do. -``` +```java // bad @Test public void testFooBar() { @@ -487,7 +487,7 @@ public void testBar() { This also applies to cases where same method is tested with different arguments inside single unit test. Note: This represents the replacement we have selected for parameterized testing. -``` +```java // bad @Test public void testFooFirstSecond() { @@ -527,7 +527,7 @@ It is also recommended to structure test method names with this scheme: name of method that is being tested, word `should`, what a method should return. If a method should return something based on a certain condition, add word `when` and description of a condition. -``` +```java // bad @Test public void doSomethingTest() { @@ -547,7 +547,7 @@ public void processDataShouldReturnResultWhenInputIsData() { Place data used in test as close as possible to test code. This will make tests easier to read, review and understand. -``` +```java // bad @Test public void testFoo() { @@ -576,7 +576,7 @@ This point also implies the next one. Since we are trying to improve test simplicity and readability and place test data close to tests, we decided to avoid usage of top level constants where it is possible. Instead, just inline constant values. -``` +```java // bad public class TestClass { @@ -609,7 +609,7 @@ public class TestClass { Don't use real information in tests, like existing endpoint URLs, account IDs, etc. -``` +```java // bad String ENDPOINT_URL = "https://prebid.org"; From cd14f9d4b987a1ffcb104bebb936f23b12614a19 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:58:55 +0100 Subject: [PATCH 132/170] Add Debug Metrics (#3548) --- docs/metrics.md | 2 + .../server/auction/ExchangeService.java | 8 +- .../org/prebid/server/metric/MetricName.java | 1 + .../org/prebid/server/metric/Metrics.java | 14 ++ .../server/functional/tests/DebugSpec.groovy | 186 +++++++++++++++++- .../server/auction/ExchangeServiceTest.java | 2 + .../org/prebid/server/metric/MetricsTest.java | 26 +++ 7 files changed, 236 insertions(+), 3 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index 504cd50f5db..85df92bc269 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -37,6 +37,7 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. ## General auction metrics - `app_requests` - number of requests received from applications +- `debug_requests` - number of requests received (when debug mode is enabled) - `no_cookie_requests` - number of requests without `uids` cookie or with one that didn't contain at least one live UID - `request_time` - timer tracking how long did it take for Prebid Server to serve a request - `imps_requested` - number if impressions requested @@ -89,6 +90,7 @@ Following metrics are collected and submitted if account is configured with `bas Following metrics are collected and submitted if account is configured with `detailed` verbosity: - `account..requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `` broken down by type of incoming request +- `account..debug_requests` - number of requests received from account with `` broken down by type of incoming request (when debug mode is enabled) - `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` - `account..adapter..request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` - `account..adapter..bids_received` - number of bids received from `` when incoming request was from `` diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 7e37c48f219..6fff31c6459 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -249,6 +249,10 @@ private Future runAuction(AuctionContext receivedContext) { final Map bidderToMultiBid = bidderToMultiBids(bidRequest, debugWarnings); receivedContext.getBidRejectionTrackers().putAll(makeBidRejectionTrackers(bidRequest, aliases)); + final boolean debugEnabled = receivedContext.getDebugContext().isDebugEnabled(); + metrics.updateDebugRequestMetrics(debugEnabled); + metrics.updateAccountDebugRequestMetrics(account, debugEnabled); + return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), timeout) .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses)) .compose(storedResponseResult -> @@ -274,7 +278,7 @@ private Future runAuction(AuctionContext receivedContext) { bidRequest.getImp(), context.getBidRejectionTrackers())) .map(auctionParticipations -> dropZeroNonDealBids( - auctionParticipations, debugWarnings, context.getDebugContext().isDebugEnabled())) + auctionParticipations, debugWarnings, debugEnabled)) .map(auctionParticipations -> bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases)) .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases)) @@ -285,7 +289,7 @@ private Future runAuction(AuctionContext receivedContext) { logger, bidResponse, context.getBidRequest(), - context.getDebugContext().isDebugEnabled())) + debugEnabled)) .compose(bidResponse -> bidResponsePostProcessor.postProcess( context.getHttpRequest(), uidsCookie, bidRequest, bidResponse, account)) .map(context::with)); diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 7f27ea2212e..9ea3d15f0fc 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -25,6 +25,7 @@ public enum MetricName { // auction requests, + debug_requests, app_requests, no_cookie_requests, request_time, diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index c435e361fec..52964a9f8b0 100644 --- a/src/main/java/org/prebid/server/metric/Metrics.java +++ b/src/main/java/org/prebid/server/metric/Metrics.java @@ -167,6 +167,12 @@ HooksMetrics hooks() { return hooksMetrics; } + public void updateDebugRequestMetrics(boolean debugEnabled) { + if (debugEnabled) { + incCounter(MetricName.debug_requests); + } + } + public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, int numImps) { if (isApp) { incCounter(MetricName.app_requests); @@ -235,12 +241,20 @@ public void updateAccountRequestMetrics(Account account, MetricName requestType) final AccountMetrics accountMetrics = forAccount(account.getId()); accountMetrics.incCounter(MetricName.requests); + if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed)) { accountMetrics.requestType(requestType).incCounter(MetricName.requests); } } } + public void updateAccountDebugRequestMetrics(Account account, boolean debugEnabled) { + final AccountMetricsVerbosityLevel verbosityLevel = accountMetricsVerbosityResolver.forAccount(account); + if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed) && debugEnabled) { + forAccount(account.getId()).incCounter(MetricName.debug_requests); + } + } + public void updateAccountRequestRejectedByInvalidAccountMetrics(String accountId) { updateAccountRequestsMetrics(accountId, MetricName.rejected_by_invalid_account); } diff --git a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy index 715c20c9dcb..ea169ad00ee 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy @@ -3,18 +3,24 @@ package org.prebid.server.functional.tests import org.apache.commons.lang3.StringUtils import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountMetricsConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.BASIC +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.NONE import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.response.auction.BidderCallType.STORED_BID_RESPONSE @@ -22,19 +28,33 @@ import static org.prebid.server.functional.model.response.auction.BidderCallType class DebugSpec extends BaseSpec { private static final String overrideToken = PBSUtils.randomString + private static final String ACCOUNT_METRICS_PREFIX_NAME = "account" + private static final String DEBUG_REQUESTS_METRIC = "debug_requests" + private static final String ACCOUNT_DEBUG_REQUESTS_METRIC = "account.%s.debug_requests" + private static final String REQUEST_OK_WEB_METRICS = "requests.ok.openrtb2-web" - def "PBS should return debug information when debug flag is #debug and test flag is #test"() { + def "PBS should return debug information and emit metrics when debug flag is #debug and test flag is #test"() { given: "Default BidRequest with test flag" def bidRequest = BidRequest.defaultBidRequest bidRequest.ext.prebid.debug = debug bidRequest.test = test + and: "Flash metrics" + flushMetrics(defaultPbsService) + when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Response should contain ext.debug" assert response.ext?.debug + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + and: "Account debug metrics shouldn't be incremented" + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + where: debug | test ENABLED | null @@ -48,12 +68,23 @@ class DebugSpec extends BaseSpec { bidRequest.ext.prebid.debug = test bidRequest.test = test + and: "Flash metrics" + flushMetrics(defaultPbsService) + when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Response shouldn't contain ext.debug" assert !response.ext?.debug + and: "Debug metrics shouldn't be populated" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[DEBUG_REQUESTS_METRIC] + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + + and: "General metrics should be present" + assert metricsRequest[REQUEST_OK_WEB_METRICS] == 1 + where: debug | test DISABLED | null @@ -351,4 +382,157 @@ class DebugSpec extends BaseSpec { and: "Response should not contain ext.warnings" assert !response.ext?.warnings } + + def "PBS should return debug information and emit metrics when account debug enabled and verbosity detailed"() { + given: "Default basic generic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: DETAILED), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] == 1 + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + } + + def "PBS shouldn't return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() { + given: "Default basic generic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Account debug metrics shouldn't be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] + + and: "Request debug metrics should be incremented" + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + where: + verbosityLevel << [NONE, BASIC] + } + + def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity detailed"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: DETAILED), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] == 1 + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + } + + def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Account debug metrics shouldn't be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] + + and: "Debug metrics should be incremented" + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + where: + verbosityLevel << [NONE, BASIC] + } + + def "PBS shouldn't emit auction request metric when incoming request invalid"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null) + bidRequest.ext.prebid.debug = ENABLED + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.responseBody.contains("request.site should include at least one of request.site.id or request.site.page") + + and: "Debug metrics shouldn't be populated" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[DEBUG_REQUESTS_METRIC] + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + + and: "General metrics shouldn't be present" + assert !metricsRequest[REQUEST_OK_WEB_METRICS] + } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index e8d78c33b1a..c50377f0667 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -2979,6 +2979,8 @@ public void shouldIncrementCommonMetrics() { target.holdAuction(givenRequestContext(bidRequest)); // then + verify(metrics).updateDebugRequestMetrics(false); + verify(metrics).updateAccountDebugRequestMetrics(any(), eq(false)); verify(metrics).updateRequestBidderCardinalityMetric(1); verify(metrics).updateAccountRequestMetrics(any(), eq(MetricName.openrtb2web)); verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics( diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java index 104df273c3e..f4943895116 100644 --- a/src/test/java/org/prebid/server/metric/MetricsTest.java +++ b/src/test/java/org/prebid/server/metric/MetricsTest.java @@ -331,6 +331,16 @@ public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics() assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(4); } + @Test + public void updateDebugRequestsMetricsShouldIncrementMetrics() { + // when + metrics.updateDebugRequestMetrics(false); + metrics.updateDebugRequestMetrics(true); + + // then + assertThat(metricRegistry.counter("debug_requests").getCount()).isOne(); + } + @Test public void updateImpTypesMetricsByCountPerMediaTypeShouldIncrementMetrics() { // given @@ -427,6 +437,16 @@ public void updateAccountRequestMetricsShouldIncrementMetrics() { assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne(); } + @Test + public void updateAccountDebugRequestMetricsShouldIncrementMetrics() { + // when + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true); + + // then + assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isOne(); + } + @Test public void updateAdapterRequestTypeAndNoCookieMetricsShouldUpdateMetricsAsExpected() { @@ -916,6 +936,8 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() { given(accountMetricsVerbosityResolver.forAccount(any())).willReturn(AccountMetricsVerbosityLevel.none); // when + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true); metrics.updateAccountRequestMetrics(Account.empty(ACCOUNT_ID), MetricName.openrtb2web); metrics.updateAdapterResponseTime(RUBICON, Account.empty(ACCOUNT_ID), 500); metrics.updateAdapterRequestNobidMetrics(RUBICON, Account.empty(ACCOUNT_ID)); @@ -924,6 +946,7 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() { // then assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isZero(); + assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); @@ -939,6 +962,8 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() { // when metrics.updateAccountRequestMetrics(Account.empty(ACCOUNT_ID), MetricName.openrtb2web); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false); + metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true); metrics.updateAdapterResponseTime(RUBICON, Account.empty(ACCOUNT_ID), 500); metrics.updateAdapterRequestNobidMetrics(RUBICON, Account.empty(ACCOUNT_ID)); metrics.updateAdapterRequestGotbidsMetrics(RUBICON, Account.empty(ACCOUNT_ID)); @@ -946,6 +971,7 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() { // then assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne(); + assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero(); assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero(); assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero(); From 4da80ac75d8166767ba60a13006ea4219d9a17da Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:00:30 +0200 Subject: [PATCH 133/170] Update Gothamads account passing (#3555) --- .../org/prebid/server/bidder/gotthamads/GothamAdsBidder.java | 2 +- .../org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java index e444038f329..62b0ad34155 100644 --- a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java @@ -34,7 +34,7 @@ public class GothamAdsBidder implements Bidder { private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; - private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; + private static final String ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String X_OPENRTB_VERSION = "2.5"; private final String endpointUrl; diff --git a/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java b/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java index 1a083ed3522..41c38db12ad 100644 --- a/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java @@ -46,7 +46,7 @@ public class GothamAdsBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test-url.com/?pass={{AccountId}}"; + private static final String ENDPOINT_URL = "https://test-url.com/?pass={{AccountID}}"; private final GothamAdsBidder target = new GothamAdsBidder(ENDPOINT_URL, jacksonMapper); From 2de8e6b836a90475d7488b9934add22c7070e1be Mon Sep 17 00:00:00 2001 From: Compile-Ninja Date: Mon, 25 Nov 2024 15:02:41 +0100 Subject: [PATCH 134/170] Krushmedia: USersync update (#3556) --- src/main/resources/bidder-config/krushmedia.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/resources/bidder-config/krushmedia.yaml b/src/main/resources/bidder-config/krushmedia.yaml index 46b2d2aa47e..0f2a42e7785 100644 --- a/src/main/resources/bidder-config/krushmedia.yaml +++ b/src/main/resources/bidder-config/krushmedia.yaml @@ -16,6 +16,10 @@ adapters: usersync: cookie-family-name: krushmedia redirect: - url: https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}} + url: https://cs.krushmedia.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} support-cors: false - uid-macro: '[uid]' + uid-macro: '[UID]' + iframe: + url: https://cs.krushmedia.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + support-cors: false + uid-macro: '[UID]' From 73e2f8fd10a4c42baff957755080810d5bcaaa62 Mon Sep 17 00:00:00 2001 From: Bugxyb Date: Mon, 25 Nov 2024 22:39:54 +0800 Subject: [PATCH 135/170] add Aglorix GVL Vendor ID (#3571) --- src/main/resources/bidder-config/algorix.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/algorix.yaml b/src/main/resources/bidder-config/algorix.yaml index f76db6420ce..14d31df58d6 100644 --- a/src/main/resources/bidder-config/algorix.yaml +++ b/src/main/resources/bidder-config/algorix.yaml @@ -9,4 +9,4 @@ adapters: - native site-media-types: supported-vendors: - vendor-id: 0 + vendor-id: 1176 From 32678256f12923c87c4e5d79480dd8379d825189 Mon Sep 17 00:00:00 2001 From: bretg Date: Tue, 26 Nov 2024 10:42:35 -0500 Subject: [PATCH 136/170] Update code-reviews.md (#3560) * Update code-reviews.md * Update code-reviews.md * Update code-reviews.md --- docs/developers/code-reviews.md | 38 ++++++++++++++------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md index cc8ed667849..ba7fb0ee526 100644 --- a/docs/developers/code-reviews.md +++ b/docs/developers/code-reviews.md @@ -3,33 +3,21 @@ ## Standards Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server-java/pulls). -All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge. - -Very small pull requests may be merged with just one review if they: - -1. Do not change the public API. -2. Have low risk of bugs, in the opinion of the reviewer. -3. Introduce no new features, or impact the code architecture. - -Larger pull requests must meet at least one of the following two additional requirements. - -1. Have a second approval from a core member -2. Be open for 5 business days with no new changes requested. +1. PRs that touch only adapters and modules can be approved by one reviewer before merge. +2. PRs that touch PBS-core must be reviewed and approved by at least two 'core' reviewers before merge. ## Process -New pull requests should be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) -to a core member for review within 3 business days of being opened. -That person should either approve the changes or request changes within 4 business days of being assigned. -If they're too busy, they should assign it to someone else who can review it within that timeframe. +New pull requests must be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) +to a reviewer within 5 business days of being opened. That person must either approve the changes or request changes within 5 business days of being assigned. + +If a reviewer is too busy, they should re-assign it to someone else as soon as possible so that person has enough time to take over the review and still meet the 5-day goal. Please tag the new reviewer in the PR. If you don't know who to assign it to, use the #prebid-server-java-dev Slack channel to ask for help in re-assigning. -If the changes are small, that member can merge the PR once the changes are complete. Otherwise, they should -assign the pull request to another member for a second review. +If a reviewer is going to be unavailable for more than a few days, they should update the notes column of the duty spreadsheet or drop a note about their availability into the Slack channel. -The pull request can then be merged whenever the second reviewer approves, or if 5 business days pass with no farther -changes requested by anybody, whichever comes first. +After the review, if the PR touches PBS-core, it must be assigned to a second reviewer. -## Priorities +## Review Priorities Code reviews should focus on things which cannot be validated by machines. @@ -43,4 +31,10 @@ explaining it. Are there better ways to achieve those goals? - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead! - Can the code be organized into smaller, more modular pieces? - Is there dead code which can be deleted? Or TODO comments which should be resolved? -- Look for code used by other adapters. Encourage adapter submitter to utilize common code. +- Look for code used by other adapters. Encourage adapter submitter to utilize common code. +- Specific bid adapter rules: + - The email contact must work and be a group, not an individual. + - Host endpoints cannot be fully dynamic. i.e. they can utilize "https://REGION.example.com", but not "https://HOST". + - They cannot _require_ a "region" parameter. Region may be an optional parameter, but must have a default. + - No direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job* + - If the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter* From 5643eae9be34de4a069cab6849a07d137ec251a9 Mon Sep 17 00:00:00 2001 From: Abdullah Al Mamun Oronno Date: Thu, 28 Nov 2024 08:23:44 -0500 Subject: [PATCH 137/170] IX: fix ix adapter handling of paapi config (#3563) --- .../org/prebid/server/bidder/ix/IxBidder.java | 11 +++-- .../response/AuctionConfigExtBidResponse.java | 14 +++++++ .../ix/model/response/IxExtBidResponse.java | 7 ++-- .../prebid/server/bidder/ix/IxBidderTest.java | 4 +- .../openrtb2/ix/test-auction-ix-request.json | 1 + .../openrtb2/ix/test-auction-ix-response.json | 42 ++++++++++++++++++- .../it/openrtb2/ix/test-ix-bid-request.json | 1 + .../it/openrtb2/ix/test-ix-bid-response.json | 40 +++++++++++++++++- 8 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java diff --git a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java index 5c7c468af8f..5fb26e698fd 100644 --- a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java @@ -409,11 +409,14 @@ private static ExtBidPrebidVideo videoInfo(ExtBidPrebidVideo extBidPrebidVideo) private List extractFledge(IxBidResponse bidResponse) { return Optional.ofNullable(bidResponse) .map(IxBidResponse::getExt) - .map(IxExtBidResponse::getFledgeAuctionConfigs) - .orElse(Collections.emptyMap()) - .entrySet() + .map(IxExtBidResponse::getProtectedAudienceAuctionConfigs) + .orElse(Collections.emptyList()) .stream() - .map(e -> FledgeAuctionConfig.builder().impId(e.getKey()).config(e.getValue()).build()) + .filter(Objects::nonNull) + .map(ixAuctionConfig -> FledgeAuctionConfig.builder() + .impId(ixAuctionConfig.getBidId()) + .config(ixAuctionConfig.getConfig()) + .build()) .toList(); } } diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java new file mode 100644 index 00000000000..709fab87429 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java @@ -0,0 +1,14 @@ +package org.prebid.server.bidder.ix.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AuctionConfigExtBidResponse { + + @JsonProperty("bidId") + String bidId; + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java index c292317d22c..c586817df2c 100644 --- a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java @@ -1,15 +1,14 @@ package org.prebid.server.bidder.ix.model.response; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; -import java.util.Map; +import java.util.List; @Value(staticConstructor = "of") public class IxExtBidResponse { - @JsonProperty("fledge_auction_configs") - Map fledgeAuctionConfigs; + @JsonProperty("protectedAudienceAuctionConfigs") + List protectedAudienceAuctionConfigs; } diff --git a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java index 18f020f2bc0..35726f5a205 100644 --- a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java @@ -27,6 +27,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.ix.model.request.IxDiag; +import org.prebid.server.bidder.ix.model.response.AuctionConfigExtBidResponse; import org.prebid.server.bidder.ix.model.response.IxBidResponse; import org.prebid.server.bidder.ix.model.response.IxExtBidResponse; import org.prebid.server.bidder.ix.model.response.NativeV11Wrapper; @@ -49,7 +50,6 @@ import org.prebid.server.version.PrebidVersionProvider; import java.util.List; -import java.util.Map; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -778,7 +778,7 @@ public void makeBidderResponseShouldReturnFledgeAuctionConfig() throws JsonProce final IxBidResponse bidResponseWithFledge = IxBidResponse.builder() .cur(bidResponse.getCur()) .seatbid(bidResponse.getSeatbid()) - .ext(IxExtBidResponse.of(Map.of(impId, fledgeAuctionConfig))) + .ext(IxExtBidResponse.of(List.of(AuctionConfigExtBidResponse.of(impId, fledgeAuctionConfig)))) .build(); final BidderCall httpCall = givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponseWithFledge)); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json index 0c46e680fbc..1fc85c3cd79 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json @@ -8,6 +8,7 @@ "h": 250 }, "ext": { + "ae": 1, "ix": { "siteId": "10002" } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json index 8685c8ede1a..5be6fc4788c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json @@ -27,7 +27,47 @@ "ix": "{{ ix.response_time_ms }}" }, "prebid": { - "auctiontimestamp": 0 + "auctiontimestamp": 0, + "fledge": { + "auctionconfigs": [ + { + "impid": "imp_id", + "bidder": "ix", + "adapter": "ix", + "config": { + "seller": "https://test.casalemedia.com", + "decisionLogicUrl": "https://test.casalemedia.com/decision-logic.js", + "trustedScoringSignalsURL": "https://test.casalemedia.com/123", + "interestGroupBuyers": [ + "https://test.com" + ], + "sellerSignals": { + "callbackURL": "https://test.casalemedia.com/callback/1", + "debugURL": "https://test.casalemedia.com/debug/1", + "width": 300, + "height": 250 + }, + "sellerTimeout": 150, + "perBuyerSignals": { + "https://test.com": [ + { + "key": "value" + } + ] + }, + "perBuyerCurrencies": { + "*": "USD" + }, + "sellerCurrency": "USD", + "requestedSize": { + "width": 300, + "height": 250 + }, + "maxTrustedBiddingSignalsURLLength": 1000 + } + } + ] + } }, "tmaxrequest": 5000 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json index 0658f90c813..ef303b14b8e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json @@ -15,6 +15,7 @@ "h": 250 }, "ext": { + "ae": 1, "tid": "${json-unit.any-string}", "bidder": { "siteId": "10002" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json index 9d9d7035ed7..c62e7c626b9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json @@ -13,5 +13,43 @@ ], "seat": "seatId6" } - ] + ], + "ext": { + "protectedAudienceAuctionConfigs": [ + { + "bidId": "imp_id", + "config": { + "seller": "https://test.casalemedia.com", + "decisionLogicUrl": "https://test.casalemedia.com/decision-logic.js", + "trustedScoringSignalsURL": "https://test.casalemedia.com/123", + "interestGroupBuyers": [ + "https://test.com" + ], + "sellerSignals": { + "callbackURL": "https://test.casalemedia.com/callback/1", + "debugURL": "https://test.casalemedia.com/debug/1", + "width": 300, + "height": 250 + }, + "sellerTimeout": 150, + "perBuyerSignals": { + "https://test.com": [ + { + "key": "value" + } + ] + }, + "perBuyerCurrencies": { + "*": "USD" + }, + "sellerCurrency": "USD", + "requestedSize": { + "width": 300, + "height": 250 + }, + "maxTrustedBiddingSignalsURLLength": 1000 + } + } + ] + } } From 79bc0557dfed094e19537746ecd1ae5ee48bc700 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Fri, 29 Nov 2024 06:45:23 -0500 Subject: [PATCH 138/170] Housekeeping: Update pull_request_template.md (#3585) --- .github/pull_request_template.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 28ce307df13..a5f5cdf5aaf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,40 +1,34 @@ ### 🔧 Type of changes - [ ] new bid adapter -- [ ] update bid adapter +- [ ] bid adapter update - [ ] new feature - [ ] new analytics adapter - [ ] new module +- [ ] module update - [ ] bugfix - [ ] documentation - [ ] configuration +- [ ] dependency update - [ ] tech debt (test coverage, refactorings, etc.) ### ✨ What's the context? - -What's the context for the changes? Are there any - +What's the context for the changes? ### 🧠 Rationale behind the change - Why did you choose to make these changes? Were there any trade-offs you had to consider? - ### 🔎 New Bid Adapter Checklist - [ ] verify email contact works -- [ ] NO fully dynamic hosts +- [ ] NO fully dynamic hostnames - [ ] geographic host parameters are NOT required -- [ ] NO direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job* +- [ ] direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job* - [ ] if the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter* - [ ] cover an adapter configuration with an integration test - ### 🧪 Test plan - How do you know the changes are safe to ship to production? - ### 🏎 Quality check - - [ ] Are your changes following [our code style guidelines](https://github.com/prebid/prebid-server-java/blob/master/docs/developers/code-style.md)? - [ ] Are there any breaking changes in your code? - [ ] Does your test coverage exceed 90%? From 36b408d94cfc7f55d799131ae6c20a0ee42cb86e Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:45:59 +0100 Subject: [PATCH 139/170] Richaudience Bidder: Add Redirect Sync (#3578) --- src/main/resources/bidder-config/richaudience.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/bidder-config/richaudience.yaml b/src/main/resources/bidder-config/richaudience.yaml index ebe84aec464..b691d330734 100644 --- a/src/main/resources/bidder-config/richaudience.yaml +++ b/src/main/resources/bidder-config/richaudience.yaml @@ -17,3 +17,7 @@ adapters: url: https://sync.richaudience.com/74889303289e27f327ad0c6de7be7264/?consentString={{gdpr_consent}}&r={{redirect_url}} support-cors: false uid-macro: '[PDID]' + redirect: + url: https://sync.richaudience.com/f7872c90c5d3791e2b51f7edce1a0a5d/?p=pbs&consentString={{gdpr_consent}}&r={{redirect_url}} + support-cors: false + uid-macro: '[PDID]' From 77b1f6fcc336a97d7f06582c53593c3d580ee372 Mon Sep 17 00:00:00 2001 From: Jeff Mahoney Date: Tue, 3 Dec 2024 08:08:23 -0500 Subject: [PATCH 140/170] Sharethrough: declare support for oRTB 2.6 (#3561) --- src/main/resources/bidder-config/sharethrough.yaml | 1 + .../sharethrough/test-sharethrough-bid-request.json | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/resources/bidder-config/sharethrough.yaml b/src/main/resources/bidder-config/sharethrough.yaml index 36cc0f0127f..77bcce9d31f 100644 --- a/src/main/resources/bidder-config/sharethrough.yaml +++ b/src/main/resources/bidder-config/sharethrough.yaml @@ -1,6 +1,7 @@ adapters: sharethrough: endpoint: https://btlr.sharethrough.com/universal/v1?supply_id=FGMrCMMc + ortb-version: '2.6' meta-info: maintainer-email: pubgrowth.engineering@sharethrough.com app-media-types: diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json index 336cd9f774b..fa6695a0791 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json @@ -33,9 +33,7 @@ }, "at": 1, "tmax": "${json-unit.any-number}", - "cur": [ - "USD" - ], + "cur": ["USD"], "source": { "tid": "${json-unit.any-string}", "ext": { @@ -44,9 +42,7 @@ } }, "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 }, "ext": { "prebid": { From 5fa096a0868c4916bf0f74a22a8f02da9f107610 Mon Sep 17 00:00:00 2001 From: Rishi Parmar Date: Tue, 3 Dec 2024 18:40:31 +0530 Subject: [PATCH 141/170] Add iframe sync for medianet (#3586) --- src/main/resources/bidder-config/medianet.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/bidder-config/medianet.yaml b/src/main/resources/bidder-config/medianet.yaml index 63aecb5f2b0..ceb545af062 100644 --- a/src/main/resources/bidder-config/medianet.yaml +++ b/src/main/resources/bidder-config/medianet.yaml @@ -17,6 +17,10 @@ adapters: vendor-id: 142 usersync: cookie-family-name: medianet + iframe: + url: https://hbx.media.net/checksync.php?cid=8CUEHS6F9&cs=87&type=mpbc&cv=37&vsSync=1&uspstring={{us_privacy}}&gdpr={{gdpr}}&gdprsting={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '' redirect: url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false From f6276c9ea97f5e7511deb7658e55d857e53366d4 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Tue, 3 Dec 2024 16:28:18 +0200 Subject: [PATCH 142/170] Amp: update consented providers settings support (#3590) --- .../server/auction/requestfactory/AmpRequestFactory.java | 1 + .../prebid/server/proto/openrtb/ext/request/ExtUser.java | 5 +++++ .../auction/requestfactory/AmpRequestFactoryTest.java | 4 +++- .../bidder/improvedigital/ImprovedigitalBidderTest.java | 6 +++--- .../org/prebid/server/it/amp/test-generic-bid-request.json | 3 +++ .../prebid/server/it/amp/test-genericAlias-bid-request.json | 3 +++ 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index b014c508678..fb8187ed231 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -274,6 +274,7 @@ private static User createUser(ConsentParam consentParam, String addtlConsent) { final ExtUser extUser = consentedProvidersSettings != null ? ExtUser.builder() + .deprecatedConsentedProvidersSettings(consentedProvidersSettings) .consentedProvidersSettings(consentedProvidersSettings) .build() : null; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java index 900a355a9fb..c570e221362 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java @@ -55,8 +55,13 @@ public class ExtUser extends FlexibleExtension { /** * Defines the contract for bidrequest.user.ext.ConsentedProvidersSettings + *

+ * TODO: Remove after PBS 4.0 */ + @Deprecated(forRemoval = true) @JsonProperty("ConsentedProvidersSettings") + ConsentedProvidersSettings deprecatedConsentedProvidersSettings; + ConsentedProvidersSettings consentedProvidersSettings; @JsonIgnore diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index 9b716645aa7..1e0f63b9192 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -1277,10 +1277,12 @@ public void shouldReturnBidRequestWithProvidersSettingsContainsAddtlConsentIfPar final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest(); // then + final ConsentedProvidersSettings settings = ConsentedProvidersSettings.of("someConsent"); assertThat(result.getUser()) .isEqualTo(User.builder() .ext(ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("someConsent")) + .deprecatedConsentedProvidersSettings(settings) + .consentedProvidersSettings(settings) .build()) .build()); } diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java index 37ab6635458..82c9ff1e659 100644 --- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java @@ -116,7 +116,7 @@ public void makeHttpRequestsShouldUseProperEndpoints() { public void makeHttpRequestsShouldProperProcessConsentedProvidersSetting() { // given final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90")) + .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90")) .build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder @@ -145,7 +145,7 @@ public void makeHttpRequestsShouldProperProcessConsentedProvidersSetting() { public void makeHttpRequestsShouldProperProcessConsentedProvidersSettingWithMultipleTilda() { // given final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90~anything")) + .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90~anything")) .build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder @@ -174,7 +174,7 @@ public void makeHttpRequestsShouldProperProcessConsentedProvidersSettingWithMult public void makeHttpRequestsShouldReturnUserExtIfConsentedProvidersIsNotProvided() { // given final ExtUser extUser = ExtUser.builder() - .consentedProvidersSettings(ConsentedProvidersSettings.of(null)) + .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of(null)) .build(); final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> diff --git a/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json index 5cc33c6206c..4d45a82bbc0 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json @@ -51,6 +51,9 @@ "ext": { "ConsentedProvidersSettings": { "consented_providers": "someConsent" + }, + "consented_providers_settings": { + "consented_providers": "someConsent" } } }, diff --git a/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json index 1d28937fef6..65febdb9a16 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json @@ -49,6 +49,9 @@ "ext": { "ConsentedProvidersSettings": { "consented_providers": "someConsent" + }, + "consented_providers_settings": { + "consented_providers": "someConsent" } } }, From 9cb9a837219a3284a6ca49e46cb6e1b2de21854c Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:35:57 +0100 Subject: [PATCH 143/170] Use video.plcmt to Resolve Media Type (#3572) --- .../server/auction/ImpMediaTypeResolver.java | 8 +- .../BidAdjustmentsProcessor.java | 75 +++--- .../functional/tests/BidAdjustmentSpec.groovy | 232 +++++++++++++++--- .../BidAdjustmentsProcessorTest.java | 71 +++++- .../test-auction-generic-request.json | 3 +- .../test-generic-bid-request.json | 1 + 6 files changed, 303 insertions(+), 87 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java index 3256ed360e1..964b89b8b3e 100644 --- a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java +++ b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java @@ -31,12 +31,14 @@ private static ImpMediaType resolveBidAdjustmentVideoMediaType(String bidImpId, .orElse(null); if (bidImpVideo == null) { - return null; + return ImpMediaType.video_outstream; } final Integer placement = bidImpVideo.getPlacement(); - return placement == null || Objects.equals(placement, 1) - ? ImpMediaType.video + final Integer plcmt = bidImpVideo.getPlcmt(); + + return Objects.equals(placement, 1) || Objects.equals(plcmt, 1) + ? ImpMediaType.video_instream : ImpMediaType.video_outstream; } } diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java index 8bd10535b98..1136876c7f6 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -82,17 +82,6 @@ public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionP return auctionParticipation.with(updatedBidderResponse); } - private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); - return prebid != null ? prebid.getBidadjustmentfactors() : null; - } - - private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { - return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 - ? price.multiply(priceAdjustmentFactor) - : price; - } - private BidderBid applyBidAdjustments(BidderBid bidderBid, BidRequest bidRequest, String bidder, @@ -100,17 +89,26 @@ private BidderBid applyBidAdjustments(BidderBid bidderBid, List errors) { try { final Price originalPrice = getOriginalPrice(bidderBid); + + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), + bidRequest.getImp(), + bidderBid.getType()); + final Price priceWithFactorsApplied = applyBidAdjustmentFactors( originalPrice, - bidderBid, bidder, - bidRequest); + bidRequest, + mediaType); + final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules( priceWithFactorsApplied, - bidderBid, bidder, bidRequest, - bidAdjustments); + bidAdjustments, + mediaType, + bidderBid.getBid().getDealid()); + return updateBid(originalPrice, priceWithAdjustmentsApplied, bidderBid, bidRequest); } catch (PreBidException e) { errors.add(BidderError.generic(e.getMessage())); @@ -154,51 +152,54 @@ private Price getOriginalPrice(BidderBid bidderBid) { return Price.of(StringUtils.stripToNull(bidCurrency), price); } - private Price applyBidAdjustmentFactors(Price bidPrice, BidderBid bidderBid, String bidder, BidRequest bidRequest) { + private Price applyBidAdjustmentFactors(Price bidPrice, + String bidder, + BidRequest bidRequest, + ImpMediaType mediaType) { + final String bidCurrency = bidPrice.getCurrency(); final BigDecimal price = bidPrice.getValue(); - final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidder, bidRequest, bidderBid); + final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidder, bidRequest, mediaType); final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, price); return Price.of(bidCurrency, adjustedPrice.compareTo(price) != 0 ? adjustedPrice : price); } - private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { + private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, ImpMediaType mediaType) { final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); if (adjustmentFactors == null) { return null; } - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bidderBid.getBid().getImpid(), - bidRequest.getImp(), - bidderBid.getType()); + final ImpMediaType targetMediaType = mediaType == ImpMediaType.video_instream ? ImpMediaType.video : mediaType; + return bidAdjustmentFactorResolver.resolve(targetMediaType, adjustmentFactors, bidder); + } + + private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); + return prebid != null ? prebid.getBidadjustmentfactors() : null; + } - return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { + return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 + ? price.multiply(priceAdjustmentFactor) + : price; } private Price applyBidAdjustmentRules(Price bidPrice, - BidderBid bidderBid, String bidder, BidRequest bidRequest, - BidAdjustments bidAdjustments) { - - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidPrice.getCurrency(); - final BigDecimal price = bidPrice.getValue(); - - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bid.getImpid(), - bidRequest.getImp(), - bidderBid.getType()); + BidAdjustments bidAdjustments, + ImpMediaType mediaType, + String dealId) { return bidAdjustmentsResolver.resolve( - Price.of(bidCurrency, price), + bidPrice, bidRequest, bidAdjustments, - mediaType == null || mediaType == ImpMediaType.video ? ImpMediaType.video_instream : mediaType, + mediaType, bidder, - bid.getDealid()); + dealId); } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 90c6c764929..79eb960787f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -12,6 +12,8 @@ import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors import org.prebid.server.functional.model.request.auction.BidAdjustmentRule import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes +import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService @@ -40,8 +42,8 @@ import static org.prebid.server.functional.model.request.auction.BidAdjustmentMe import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE -import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_ARTICLE -import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer import static org.prebid.server.functional.util.PBSUtils.getRandomDecimal @@ -56,6 +58,8 @@ class BidAdjustmentSpec extends BaseSpec { private static final Currency DEFAULT_CURRENCY = USD private static final int BID_ADJUST_PRECISION = 4 private static final int PRICE_PRECISION = 3 + private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) + private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, (GBP): 0.793776804452961], (GBP): [(USD): 1.2597999770088517, @@ -220,22 +224,43 @@ class BidAdjustmentSpec extends BaseSpec { where: adjustmentType | ruleValue | mediaType | bidRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest @@ -284,22 +309,43 @@ class BidAdjustmentSpec extends BaseSpec { where: adjustmentType | ruleValue | mediaType | bidRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest @@ -341,22 +387,43 @@ class BidAdjustmentSpec extends BaseSpec { where: adjustmentType | ruleValue | mediaType | bidRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest @@ -403,22 +470,43 @@ class BidAdjustmentSpec extends BaseSpec { where: adjustmentType | ruleValue | mediaType | bidRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest @@ -673,40 +761,82 @@ class BidAdjustmentSpec extends BaseSpec { where: adjustmentType | ruleValue | mediaType | bidRequest MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest - MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest - MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest CPM | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest - CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) CPM | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest CPM | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest CPM | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest CPM | MAX_CPM_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest - CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) CPM | MAX_CPM_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest CPM | MAX_CPM_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest CPM | MAX_CPM_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest STATIC | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest - STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest STATIC | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest - STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_STREAM } - STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | BidRequest.defaultVideoRequest.tap { imp.first.video.placement = IN_ARTICLE } + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest @@ -997,4 +1127,30 @@ class BidAdjustmentSpec extends BaseSpec { return originalPrice } } + + private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + placement = videoPlacementSubtypes + } + } + } + + private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + } + } + } + + private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, + VideoPlacementSubtypes videoPlacementSubtypes) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + placement = videoPlacementSubtypes + } + } + } } diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java index 33cfe50e09c..2affb167eef 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.prebid.server.VertxTest; import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionParticipation; @@ -47,7 +49,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; -import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -57,13 +58,14 @@ import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class BidAdjustmentsProcessorTest extends VertxTest { - @Mock(strictness = LENIENT) + @Mock private CurrencyConversionService currencyService; - @Mock(strictness = LENIENT) + @Mock private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; - @Mock(strictness = LENIENT) + @Mock private BidAdjustmentsResolver bidAdjustmentsResolver; private BidAdjustmentsProcessor target; @@ -469,7 +471,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP } @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlcmtEqualsOne() { // given final BidderResponse bidderResponse = BidderResponse.of( "bidder", @@ -491,6 +493,58 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) .willReturn(BigDecimal.valueOf(3.456)); + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().plcmt(1).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); + + // when + final AuctionParticipation result = target.enrichWithAdjustedBids( + auctionParticipation, bidRequest, givenBidAdjustments()); + + // then + assertThat(result.getBidderResponse().getSeatBid().getBids()) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + + verify(bidAdjustmentsResolver).resolve( + eq(Price.of("USD", BigDecimal.valueOf(6.912))), + eq(bidRequest), + eq(givenBidAdjustments()), + eq(ImpMediaType.video_instream), + eq("bidder"), + eq("dealId")); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideoPlacementAndPlcmtIsMissing() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)) + .dealid("dealId") + .build(), + "USD", video))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_outstream, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> impBuilder.id("123").video(Video.builder().build()))), builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() @@ -514,13 +568,13 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), eq(givenBidAdjustments()), - eq(ImpMediaType.video_instream), + eq(ImpMediaType.video_outstream), eq("bidder"), eq("dealId")); } @Test - public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { + public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidImpId() { // given final BidderResponse bidderResponse = BidderResponse.of( "bidder", @@ -560,11 +614,12 @@ public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { .extracting(Bid::getPrice) .containsExactly(BigDecimal.valueOf(2)); + verify(bidAdjustmentFactorResolver).resolve(ImpMediaType.video_outstream, givenAdjustments, "bidder"); verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), eq(givenBidAdjustments()), - eq(ImpMediaType.video_instream), + eq(ImpMediaType.video_outstream), eq("bidder"), eq("dealId")); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json index a0d9014043a..7dc036ee1d4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json @@ -8,7 +8,8 @@ "mimes" ], "w": 300, - "h": 250 + "h": 250, + "placement": 1 }, "ext": { "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json index d1ae2f0fce0..754ed9ddfff 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json @@ -3,6 +3,7 @@ "imp" : [ { "id": "impId001", "video": { + "placement": 1, "mimes": [ "mimes" ], From 21d487526d723bfa0069a7d9a3cae27c35e1bbac Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:39:01 +0100 Subject: [PATCH 144/170] Support Default Bids Cache TTL (#3543) --- docs/config-app.md | 1 + .../server/auction/BidResponseCreator.java | 54 +- .../prebid/server/auction/model/BidInfo.java | 2 +- .../prebid/server/cache/CoreCacheService.java | 2 +- .../spring/config/ServiceConfiguration.java | 17 +- .../model/CacheDefaultTtlProperties.java | 15 + .../tests/BidExpResponseSpec.groovy | 458 +++++++++++++--- .../tests/StoredResponseSpec.groovy | 41 +- .../auction/BidResponseCreatorTest.java | 502 +++++++++++++++++- .../server/it/amp/test-cache-request.json | 6 +- .../cache/update/test-auction-response.json | 2 + .../test-auction-33across-response.json | 1 + .../aax/test-auction-aax-response.json | 1 + .../aceex/test-auction-aceex-response.json | 1 + .../test-auction-acuityads-response.json | 1 + .../test-auction-adelement-response.json | 1 + .../adf/test-auction-adf-response.json | 2 + .../test-auction-adgeneration-response.json | 1 + .../adhese/test-auction-adhese-response.json | 1 + .../test-auction-adkernel-response.json | 1 + .../test-adkerneladn-bid-response.json | 2 +- .../test-auction-adkerneladn-response.json | 1 + .../adman/test-auction-adman-response.json | 1 + .../test-auction-admatic-response.json | 1 + .../admixer/test-admixer-bid-response.json | 2 +- .../test-auction-admixer-response.json | 1 + .../test-auction-adnuntius-response.json | 1 + .../test-auction-adocean-response.json | 1 + .../test-adoppler-bid-response-1.json | 2 +- .../test-auction-adoppler-response.json | 1 + .../adot/test-auction-adot-response.json | 3 +- .../adpone/test-adpone-bid-response.json | 2 +- .../adpone/test-auction-adpone-response.json | 1 + .../test-auction-adprime-response.json | 1 + .../test-auction-adquery-response.json | 1 + .../adrino/test-auction-adrino-response.json | 1 + .../test-auction-adsyield-response.json | 1 + .../test-adtarget-bid-response-1.json | 2 +- .../test-auction-adtarget-response.json | 1 + .../test-adtelligent-bid-response.json | 2 +- .../test-auction-adtelligent-response.json | 1 + .../test-auction-adtonos-response.json | 1 + .../test-auction-adtrgtme-response.json | 1 + .../test-auction-advangelists-response.json | 1 + .../adview/test-auction-adview-response.json | 1 + .../adxcg/test-auction-adxcg-response.json | 1 + .../test-adyoulike-bid-response.json | 2 +- .../test-auction-adyoulike-response.json | 1 + .../aidem/test-auction-aidem-response.json | 1 + .../openrtb2/aja/test-aja-bid-response.json | 2 +- .../aja/test-auction-aja-response.json | 1 + .../algorix/test-algorix-bid-response.json | 2 +- .../test-auction-algorix-response.json | 1 + .../alkimi/test-auction-alkimi-response.json | 1 + .../amx/test-auction-amx-response.json | 1 + .../test-auction-apacdex-response.json | 1 + .../appnexus/test-video-cache-request.json | 9 +- .../appush/test-auction-appush-response.json | 1 + .../aso/test-auction-aso-response.json | 1 + ...test-auction-audiencenetwork-response.json | 1 + .../test-auction-automatad-response.json | 1 + .../avocet/test-auction-avocet-response.json | 1 + .../axis/test-auction-axis-response.json | 1 + .../axonix/test-auction-axonix-response.json | 1 + .../bcmint/test-auction-bcmint-response.json | 1 + .../test-auction-beachfront-response.json | 1 + .../test-auction-beintoo-response.json | 1 + .../test-auction-bematterfull-response.json | 1 + .../test-auction-between-response.json | 1 + .../test-auction-beyondmedia-response.json | 1 + .../test-auction-bidagency-response.json | 1 + .../test-auction-bidmachine-response.json | 1 + .../test-auction-bidmatic-response.json | 1 + .../test-auction-bidmyadz-response.json | 1 + .../test-auction-bidscube-response.json | 1 + .../test-auction-bidstack-response.json | 1 + .../bigoad/test-auction-bigoad-response.json | 1 + .../blasto/test-auction-blasto-response.json | 1 + .../bliink/test-auction-bliink-response.json | 1 + .../test-auction-bluesea-response.json | 1 + .../bmtm/test-auction-bmtm-response.json | 1 + .../test-auction-boldwin-response.json | 1 + .../brave/test-auction-brave-response.json | 1 + .../bwx/test-auction-bwx-response.json | 1 + ...est-auction-cadentaperturemx-response.json | 1 + .../ccx/test-auction-ccx-response.json | 1 + .../test-auction-cointraffic-response.json | 1 + .../test-auction-coinzilla-response.json | 1 + .../test-auction-colossusssp-response.json | 1 + .../test-auction-colossus-response.json | 1 + .../test-auction-compass-response.json | 1 + .../test-auction-concert-response.json | 1 + .../test-auction-connectad-response.json | 1 + .../test-auction-consumable-response.json | 1 + .../test-auction-copper6-response.json | 1 + .../test-auction-copper6ssp-response.json | 1 + .../test-auction-cpmstar-response.json | 1 + .../criteo/test-auction-criteo-response.json | 1 + .../test-auction-datablocks-response.json | 1 + .../test-auction-decenterads-response.json | 1 + .../test-auction-deepintent-response.json | 1 + .../test-auction-definemedia-response.json | 1 + .../test-auction-dianomi-response.json | 1 + .../test-auction-displayio-response.json | 1 + .../dmx/test-auction-dmx-response.json | 1 + .../test-auction-driftpixel-response.json | 1 + .../test-auction-dxkulture-response.json | 1 + .../test-auction-edge226-response.json | 1 + .../test-auction-embimedia-response.json | 1 + .../emtv/test-auction-emtv-response.json | 1 + .../test-auction-emxdigital-response.json | 1 + .../test-auction-eplanning-response.json | 1 + .../epom/test-auction-epom-response.json | 1 + .../alias/test-auction-epsilon-response.json | 1 + .../test-auction-epsilon-response.json | 1 + .../test-auction-escalax-response.json | 1 + .../test-auction-evolution-response.json | 1 + .../test-auction-felixads-response.json | 1 + .../test-auction-filmzie-response.json | 1 + .../test-auction-finative-response.json | 1 + .../flipp/test-auction-flipp-response.json | 1 + .../test-auction-freewheelssp-response.json | 1 + .../test-auction-frvradn-response.json | 1 + .../gamma/test-auction-gamma-response.json | 1 + .../test-auction-gamoshi-response.json | 1 + .../test-auction-generic-response.json | 1 + .../test-auction-generic-response.json | 1 + .../test-cache-generic-request.json | 3 +- .../test-auction-globalsun-response.json | 1 + .../test-auction-gothamads-response.json | 1 + .../test-auction-greedygame-response.json | 1 + .../grid/test-auction-grid-response.json | 1 + .../gumgum/test-auction-gumgum-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-huaweiads-auction-response.json | 1 + .../test-auction-iionads-response.json | 1 + .../imds/test-auction-imds-response.json | 1 + .../test-auction-impactify-response.json | 1 + .../test-auction-improvedigital-response.json | 1 + .../test-auction-indicue-response.json | 1 + .../infytv/test-auction-infytv-response.json | 1 + .../inmobi/test-auction-inmobi-response.json | 1 + ...st-auction-interactiveoffers-response.json | 3 +- .../test-auction-intertech-response.json | 1 + .../test-auction-invibes-response.json | 1 + .../iqx/test-auction-iqx-response.json | 1 + .../iqzone/test-auction-iqzone-response.json | 1 + .../openrtb2/ix/test-auction-ix-response.json | 1 + .../test-auction-jdpmedia-response.json | 1 + .../jixie/test-auction-jixie-response.json | 1 + .../kargo/test-auction-kargo-response.json | 1 + .../kayzen/test-auction-kayzen-response.json | 1 + .../kidoz/test-auction-kidoz-response.json | 1 + .../test-auction-kiviads-response.json | 1 + .../test-auction-krushmedia-response.json | 1 + .../test-auction-lemmaDigital-response.json | 1 + .../test-auction-liftoff-response.json | 1 + ...est-auction-limelightDigital-response.json | 1 + .../test-auction-lmkiviads-response.json | 1 + .../test-auction-lockerdome-response.json | 1 + .../logan/test-auction-logan-response.json | 1 + .../test-auction-logicad-response.json | 1 + .../loopme/test-auction-loopme-response.json | 1 + .../loyal/test-auction-loyal-response.json | 1 + .../test-auction-lunamedia-response.json | 1 + .../test-auction-mabidder-response.json | 1 + .../test-auction-madvertise-response.json | 1 + .../test-auction-magnite-response.json | 1 + .../test-auction-markapp-response.json | 1 + .../test-auction-marsmedia-response.json | 1 + .../test-auction-mediago-response.json | 1 + .../test-auction-medianet-response.json | 1 + .../test-auction-melozen-response.json | 1 + .../metax/test-auction-metax-response.json | 1 + .../mgid/test-auction-mgid-response.json | 1 + .../mgidx/test-auction-mgidx-response.json | 1 + .../test-auction-minutemedia-response.json | 1 + .../test-auction-missena-response.json | 1 + .../test-auction-mobfoxpb-response.json | 1 + .../test-auction-mobilefuse-response.json | 1 + .../test-auction-motorik-response.json | 1 + ...auction-generic-genericAlias-response.json | 8 +- ...st-cache-generic-genericAlias-request.json | 12 +- .../test-auction-nextmillennium-response.json | 1 + .../nobid/test-auction-nobid-response.json | 1 + .../oms/test-auction-oms-response.json | 1 + .../onetag/test-auction-onetag-response.json | 1 + .../test-auction-openweb-response.json | 1 + .../openx/test-auction-openx-response.json | 1 + .../test-auction-operaads-response.json | 1 + .../oraki/test-auction-oraki-response.json | 1 + .../test-auction-orbidder-response.json | 1 + .../test-auction-outbrain-response.json | 1 + .../ownadx/test-auction-ownadx-response.json | 1 + .../pangle/test-auction-pangle-response.json | 1 + .../pgam/test-auction-pgam-response.json | 1 + .../test-auction-pgamssp-response.json | 1 + .../test-auction-playdigo-response.json | 1 + .../test-auction-preciso-response.json | 1 + .../test-auction-pubmatic-response.json | 1 + .../test-auction-pubnative-response.json | 1 + .../test-auction-pubrise-response.json | 1 + .../test-auction-pulsepoint-response.json | 1 + .../pwbid/test-auction-pwbid-response.json | 1 + .../openrtb2/qt/test-auction-qt-response.json | 1 + .../test-auction-readpeak-response.json | 1 + ...test-auction-relevantdigital-response.json | 1 + .../test-auction-resetdigital-response.json | 1 + .../test-auction-revcontent-response.json | 1 + .../test-auction-richaudience-response.json | 1 + .../rise/test-auction-rise-response.json | 1 + .../roulax/test-auction-roulax-response.json | 1 + .../test-auction-rtbhouse-response.json | 1 + .../test-auction-rubicon-response.json | 1 + .../test-auction-salunamedia-response.json | 1 + .../test-auction-screencore-response.json | 1 + ...test-auction-seedingAlliance-response.json | 1 + .../test-auction-sharethrough-response.json | 1 + .../test-auction-silvermob-response.json | 1 + .../test-auction-silverpush-response.json | 1 + .../smaato/test-auction-smaato-response.json | 1 + .../test-auction-smartadserver-response.json | 1 + .../test-auction-smarthub-response.json | 1 + .../test-auction-smartrtb-response.json | 1 + .../smartx/test-auction-smartx-response.json | 1 + .../test-auction-smartyads-response.json | 1 + .../test-auction-smilewanted-response.json | 1 + .../test-auction-smrtconnect-response.json | 1 + .../sonobi/test-auction-sonobi-response.json | 1 + .../sovrn/test-auction-sovrn-response.json | 1 + .../test-auction-sovrnxsp-response.json | 1 + .../sspbc/test-auction-sspbc-response.json | 1 + .../storedresponse/test-auction-response.json | 1 + .../storedresponse/test-cache-request.json | 3 +- .../test-auction-streamlyn-response.json | 1 + .../test-auction-stroeercore-response.json | 2 + .../test-auction-suntContent-response.json | 1 + .../test-auction-taboola-response.json | 2 + .../tappx/test-auction-tappx-response.json | 1 + .../teads/test-auction-teads-response.json | 1 + .../tgm/test-auction-tgm-response.json | 1 + .../theadx/test-auction-theadx-response.json | 1 + .../test-auction-thetradedesk-response.json | 1 + ...st-auction-thirtythreeacross-response.json | 1 + .../tpmn/test-auction-tpmn-response.json | 1 + .../test-auction-tradplus-response.json | 1 + .../test-auction-trafficgate-response.json | 1 + .../tredio/test-auction-tredio-response.json | 1 + .../test-auction-triplelift-response.json | 1 + ...st-auction-triplelift-native-response.json | 1 + .../test-auction-trustedstack-response.json | 1 + .../ttx/test-auction-ttx-response.json | 1 + .../test-auction-ucfunnel-response.json | 1 + .../test-auction-undertone-response.json | 1 + .../test-auction-unicorn-response.json | 1 + .../unruly/test-auction-unruly-response.json | 1 + .../test-auction-vidazoo-response.json | 1 + .../test-auction-videobyte-response.json | 1 + .../test-auction-videoheroes-response.json | 1 + .../test-auction-vidoomy-response.json | 1 + .../vimayx/test-auction-vimayx-response.json | 1 + ...test-auction-visiblemeasures-response.json | 1 + .../visx/test-auction-visx-response.json | 1 + .../vox/test-auction-vox-response.json | 1 + .../vrtcal/test-auction-vrtcal-response.json | 1 + .../vungle/test-auction-vungle-response.json | 1 + .../test-auction-xeworks-response.json | 1 + .../xtrmqb/test-auction-xtrmqb-response.json | 1 + .../test-auction-yahooads-response.json | 1 + .../yandex/test-auction-yandex-response.json | 1 + .../test-auction-yeahmobi-response.json | 1 + .../test-auction-yearxero-response.json | 1 + .../test-auction-yieldlab-response.json | 1 + .../test-auction-yieldmo-response.json | 1 + .../test-auction-yieldone-response.json | 1 + .../test-auction-zeroclickfraud-response.json | 1 + ...test-auction-zeta_global_ssp-response.json | 1 + .../test-auction-zmaticoo-response.json | 1 + 300 files changed, 1258 insertions(+), 177 deletions(-) create mode 100644 src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java diff --git a/docs/config-app.md b/docs/config-app.md index 40a2c42784e..7876cab6aec 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -278,6 +278,7 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a for particular publisher account. Overrides `cache.banner-ttl-seconds` property. - `cache.account..video-ttl-seconds` - how long (in seconds) video creative will be available in Cache Service for particular publisher account. Overrides `cache.video-ttl-seconds` property. +- `cache.default-ttl-seconds.{banner, video, audio, native}` - a default value how long (in seconds) a creative of the specific type will be available in Cache Service ## Application settings (account configuration, stored ad unit configurations, stored requests) Preconfigured application settings can be obtained from multiple data sources consequently: diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 31ed4ee1403..b54f8d69a73 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -95,6 +95,7 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountTargetingConfig; import org.prebid.server.settings.model.VideoStoredDataResult; +import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.util.StreamUtil; import org.prebid.server.vast.VastModifier; @@ -139,6 +140,7 @@ public class BidResponseCreator { private final Clock clock; private final JacksonMapper mapper; private final CacheTtl mediaTypeCacheTtl; + private final CacheDefaultTtlProperties cacheDefaultProperties; private final String cacheHost; private final String cachePath; @@ -156,7 +158,8 @@ public BidResponseCreator(CoreCacheService coreCacheService, int truncateAttrChars, Clock clock, JacksonMapper mapper, - CacheTtl mediaTypeCacheTtl) { + CacheTtl mediaTypeCacheTtl, + CacheDefaultTtlProperties cacheDefaultProperties) { this.coreCacheService = Objects.requireNonNull(coreCacheService); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); @@ -171,6 +174,7 @@ public BidResponseCreator(CoreCacheService coreCacheService, this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); this.mediaTypeCacheTtl = Objects.requireNonNull(mediaTypeCacheTtl); + this.cacheDefaultProperties = Objects.requireNonNull(cacheDefaultProperties); cacheHost = Objects.requireNonNull(coreCacheService.getEndpointHost()); cachePath = Objects.requireNonNull(coreCacheService.getEndpointPath()); @@ -436,8 +440,8 @@ private BidInfo toBidInfo(Bid bid, .bidType(type) .bidder(bidder) .correspondingImp(correspondingImp) - .ttl(resolveBannerTtl(bid, correspondingImp, cacheInfo, account)) - .videoTtl(type == BidType.video ? resolveVideoTtl(bid, correspondingImp, cacheInfo, account) : null) + .ttl(resolveTtl(bid, type, correspondingImp, cacheInfo, account)) + .vastTtl(type == BidType.video ? resolveVastTtl(bid, correspondingImp, cacheInfo, account) : null) .category(categoryMappingResult.getCategory(bid)) .satisfiedPriority(categoryMappingResult.isBidSatisfiesPriority(bid)) .build(); @@ -457,31 +461,43 @@ private static Optional correspondingImp(String impId, List imps) { .findFirst(); } - private Integer resolveBannerTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { - final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + private Integer resolveTtl(Bid bid, BidType type, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { final Integer bidTtl = bid.getExp(); final Integer impTtl = imp != null ? imp.getExp() : null; + final Integer requestTtl = cacheInfo.getCacheBidsTtl(); - return ObjectUtils.firstNonNull( - bidTtl, - impTtl, - cacheInfo.getCacheBidsTtl(), - accountAuctionConfig != null ? accountAuctionConfig.getBannerCacheTtl() : null, - mediaTypeCacheTtl.getBannerCacheTtl()); + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer accountTtl = accountAuctionConfig != null ? switch (type) { + case banner -> accountAuctionConfig.getBannerCacheTtl(); + case video -> accountAuctionConfig.getVideoCacheTtl(); + case audio, xNative -> null; + } : null; + + final Integer mediaTypeTtl = switch (type) { + case banner -> mediaTypeCacheTtl.getBannerCacheTtl(); + case video -> mediaTypeCacheTtl.getVideoCacheTtl(); + case audio, xNative -> null; + }; + final Integer defaultTtl = switch (type) { + case banner -> cacheDefaultProperties.getBannerTtl(); + case video -> cacheDefaultProperties.getVideoTtl(); + case audio -> cacheDefaultProperties.getAudioTtl(); + case xNative -> cacheDefaultProperties.getNativeTtl(); + }; + + return ObjectUtils.firstNonNull(bidTtl, impTtl, requestTtl, accountTtl, mediaTypeTtl, defaultTtl); } - private Integer resolveVideoTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { + private Integer resolveVastTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { final AccountAuctionConfig accountAuctionConfig = account.getAuction(); - final Integer bidTtl = bid.getExp(); - final Integer impTtl = imp != null ? imp.getExp() : null; - return ObjectUtils.firstNonNull( - bidTtl, - impTtl, + bid.getExp(), + imp != null ? imp.getExp() : null, cacheInfo.getCacheVideoBidsTtl(), accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null, - mediaTypeCacheTtl.getVideoCacheTtl()); + mediaTypeCacheTtl.getVideoCacheTtl(), + cacheDefaultProperties.getVideoTtl()); } private Future> invokeProcessedBidderResponseHooks(List bidderResponses, @@ -1369,7 +1385,7 @@ private Bid toBid(BidInfo bidInfo, final Integer ttl = Optional.ofNullable(cacheInfo) .map(info -> ObjectUtils.max(cacheInfo.getTtl(), cacheInfo.getVideoTtl())) - .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVideoTtl())); + .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVastTtl())); return bid.toBuilder() .ext(updatedBidExt) diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java index 1cb95bcf681..aa3be49fd48 100644 --- a/src/main/java/org/prebid/server/auction/model/BidInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -33,7 +33,7 @@ public class BidInfo { Integer ttl; - Integer videoTtl; + Integer vastTtl; public String getBidId() { final ObjectNode extNode = bid != null ? bid.getExt() : null; diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index e60ed70f949..e86ac4f5d9b 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -250,7 +250,7 @@ private List getCacheBids(List bidInfos) { private List getVideoCacheBids(List bidInfos) { return bidInfos.stream() .filter(bidInfo -> Objects.equals(bidInfo.getBidType(), BidType.video)) - .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVideoTtl())) + .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVastTtl())) .toList(); } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 778351b95a9..8cc22f7be13 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -110,6 +110,7 @@ import org.prebid.server.privacy.gdpr.TcfDefinerService; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.BidValidationEnforcement; +import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties; import org.prebid.server.spring.config.model.HttpClientProperties; @@ -796,6 +797,16 @@ BidderErrorNotifier bidderErrorNotifier( metrics); } + @Bean + CacheDefaultTtlProperties cacheDefaultTtlProperties( + @Value("${cache.default-ttl-seconds.banner:300}") Integer bannerTtl, + @Value("${cache.default-ttl-seconds.video:1500}") Integer videoTtl, + @Value("${cache.default-ttl-seconds.audio:1500}") Integer audioTtl, + @Value("${cache.default-ttl-seconds.native:300}") Integer nativeTtl) { + + return CacheDefaultTtlProperties.of(bannerTtl, videoTtl, audioTtl, nativeTtl); + } + @Bean BidResponseCreator bidResponseCreator( CoreCacheService coreCacheService, @@ -811,7 +822,8 @@ BidResponseCreator bidResponseCreator( Clock clock, JacksonMapper mapper, @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl, - @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl) { + @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl, + CacheDefaultTtlProperties cacheDefaultTtlProperties) { return new BidResponseCreator( coreCacheService, @@ -826,7 +838,8 @@ BidResponseCreator bidResponseCreator( truncateAttrChars, clock, mapper, - CacheTtl.of(bannerCacheTtl, videoCacheTtl)); + CacheTtl.of(bannerCacheTtl, videoCacheTtl), + cacheDefaultTtlProperties); } @Bean diff --git a/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java new file mode 100644 index 00000000000..2a3e36b6ef1 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.spring.config.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class CacheDefaultTtlProperties { + + Integer bannerTtl; + + Integer videoTtl; + + Integer audioTtl; + + Integer nativeTtl; +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy index cc877f847a0..6ca55f4b7bc 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy @@ -4,17 +4,41 @@ import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.model.request.auction.PrebidCache import org.prebid.server.functional.model.request.auction.PrebidCacheSettings import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.MediaType.BANNER +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO + class BidExpResponseSpec extends BaseSpec { - private static def hostBannerTtl = PBSUtils.randomNumber - private static def hostVideoTtl = PBSUtils.randomNumber - private static def cacheTtlService = pbsServiceFactory.getService(['cache.banner-ttl-seconds': hostBannerTtl as String, - 'cache.video-ttl-seconds' : hostVideoTtl as String]) + private static final def BANNER_TTL_HOST_CACHE = PBSUtils.randomNumber + private static final def VIDEO_TTL_HOST_CACHE = PBSUtils.randomNumber + private static final def BANNER_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def VIDEO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def AUDIO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def NATIVE_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final Map CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": BANNER_TTL_HOST_CACHE as String, + "cache.video-ttl-seconds" : VIDEO_TTL_HOST_CACHE as String] + private static final Map DEFAULT_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": BANNER_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.video" : VIDEO_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.native": NATIVE_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.audio" : AUDIO_TTL_DEFAULT_CACHE as String] + private static final Map EMPTY_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": "", + "cache.default-ttl-seconds.video" : "", + "cache.default-ttl-seconds.native": "", + "cache.default-ttl-seconds.audio" : ""] + private static final Map EMPTY_CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": "", + "cache.video-ttl-seconds" : ""] + private static def pbsOnlyHostCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + EMPTY_CACHE_TTL_CONFIG) + private static def pbsEmptyTtlService = pbsServiceFactory.getService(EMPTY_CACHE_TTL_CONFIG + EMPTY_CACHE_TTL_HOST_CONFIG) + private static def pbsHostAndDefaultCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG) + def "PBS auction should resolve bid.exp from response that is set by the bidder’s adapter"() { given: "Default basicResponse with exp" @@ -131,25 +155,6 @@ class BidExpResponseSpec extends BaseSpec { assert response.seatbid.bid.first.exp == [bidRequestExp] } - def "PBS auction shouldn't resolve exp from request.ext.prebid.cache for request when it have invalid type"() { - given: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bid response shouldn't contain exp data" - assert !response.seatbid.first.bid.first.exp - - where: - bidRequest | cache - BidRequest.defaultBidRequest | new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) - BidRequest.defaultVideoRequest | new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) - } - def "PBS auction should resolve exp from account config for banner request when it have value"() { given: "default bidRequest" def bidRequest = BidRequest.defaultBidRequest @@ -173,28 +178,6 @@ class BidExpResponseSpec extends BaseSpec { assert response.seatbid.bid.first.exp == [accountCacheTtl] } - def "PBS auction shouldn't resolve exp from account videoCacheTtl config when bidRequest type doesn't matching"() { - given: "default bidRequest" - def bidRequest = BidRequest.defaultBidRequest - - and: "Account in the DB" - def auctionConfig = new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) - def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) - accountDao.save(account) - - and: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bid response shouldn't contain exp data" - assert !response.seatbid.first.bid.first.exp - } - def "PBS auction should resolve exp from account videoCacheTtl config for video request when it have value"() { given: "default bidRequest" def bidRequest = BidRequest.defaultVideoRequest @@ -218,55 +201,15 @@ class BidExpResponseSpec extends BaseSpec { assert response.seatbid.bid.first.exp == [accountCacheTtl] } - def "PBS auction should resolve exp from account bannerCacheTtl config for video request when it have value"() { - given: "default bidRequest" - def bidRequest = BidRequest.defaultVideoRequest - - and: "Account in the DB" - def accountCacheTtl = PBSUtils.randomNumber - def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) - def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) - accountDao.save(account) - - and: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) - - then: "Bid response should contain exp data" - assert response.seatbid.bid.first.exp == [accountCacheTtl] - } - def "PBS auction should resolve exp from global banner config for banner request"() { given: "Default bidRequest" def bidRequest = BidRequest.defaultBidRequest when: "PBS processes auction request" - def response = cacheTtlService.sendAuctionRequest(bidRequest) - - then: "Bid response should contain exp data" - assert response.seatbid.bid.first.exp == [hostBannerTtl] - } - - def "PBS auction should resolve exp from global config for video request based on highest value"() { - given: "Default bidRequest" - def bidRequest = BidRequest.defaultVideoRequest - - and: "Set bidder response without exp" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].exp = null - } - bidder.setResponse(bidRequest.id, bidResponse) - - when: "PBS processes auction request" - def response = cacheTtlService.sendAuctionRequest(bidRequest) + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) then: "Bid response should contain exp data" - assert response.seatbid.bid.first.exp == [Math.max(hostVideoTtl, hostBannerTtl)] + assert response.seatbid.bid.first.exp == [BANNER_TTL_HOST_CACHE] } def "PBS auction should prioritize value from bid.exp rather than request.imp[].exp"() { @@ -356,9 +299,348 @@ class BidExpResponseSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = cacheTtlService.sendAuctionRequest(bidRequest) + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) then: "Bid response should contain exp data" assert response.seatbid.bid.first.exp == [accountCacheTtl] } + + def "PBS auction should prioritize bid.exp from the response over all other fields from the request and account config"() { + given: "Default bid request with specific imp media type" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0] = Imp.getDefaultImpression(mediaType).tap { + exp = PBSUtils.randomNumber + } + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber), + bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + and: "Default bid response with bid.exp" + def randomExp = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = randomExp + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp for #mediaType when the response, request, and account config don't include such data"() { + given: "Default bid request with specific imp media type" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response with bid.exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction should prioritize imp.exp and resolve bid.exp for #mediaType when request and account config include multiple exp sources"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType).tap { + exp = randomExp + } + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber), + bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + and: "Default bid response without bid.exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has #mediaType as mediaType"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: randomExp)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response?.seatbid?.first?.bid?.first?.exp + + where: + mediaType << [BANNER, NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has video as mediaType"() { + given: "Default bid request" + def bidsTtlSeconds = PBSUtils.randomNumber + def vastXmTtlSeconds = bidsTtlSeconds + 1 + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(VIDEO) + + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: vastXmTtlSeconds), + bids: new PrebidCacheSettings(ttlSeconds: bidsTtlSeconds)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == vastXmTtlSeconds + } + + def "PBS auction should resolve bid.exp when ext.prebid.cache.bids.ttlseconds is specified and no higher-priority fields are present"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: randomExp)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp when the account config and request imp type do not match"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType | auctionConfig + VIDEO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + VIDEO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: null) + BANNER | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + BANNER | new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + } + + def "PBS auction shouldn't resolve bid.exp when account config and request imp type match but account config for cache-ttl is not specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: null))) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [VIDEO, BANNER, NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp when account.auction.{banner/video}-cache-ttl and banner bid specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountAuctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == accountCacheTtl + + where: + mediaType | accountCacheTtl | accountAuctionConfig + BANNER | PBSUtils.randomNumber | new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + VIDEO | PBSUtils.randomNumber | new AccountAuctionConfig(videoCacheTtl: accountCacheTtl) + } + + def "PBS auction should resolve bid.exp when cache.{banner/video}-ttl-seconds config specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + enableCache() + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == expValue + + where: + mediaType | expValue + BANNER | BANNER_TTL_HOST_CACHE + VIDEO | VIDEO_TTL_HOST_CACHE + } + + def "PBS auction shouldn't resolve bid.exp when cache ttl-seconds is specified for #mediaType mediaType request"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + when: "PBS processes auction request" + def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp when cache.default-ttl-seconds.{banner,video,audio,native} is specified and no higher-priority fields are present"() { + given: "Prebid server with empty host config and default cache ttl config" + def config = EMPTY_CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG + def prebidServerService = pbsServiceFactory.getService(config) + + and: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == bidExpValue + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) + + where: + mediaType | bidExpValue + BANNER | BANNER_TTL_DEFAULT_CACHE + VIDEO | VIDEO_TTL_DEFAULT_CACHE + AUDIO | AUDIO_TTL_DEFAULT_CACHE + NATIVE | NATIVE_TTL_DEFAULT_CACHE + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy index fccb14c8bab..767c4b8e544 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy @@ -10,6 +10,7 @@ import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature @@ -17,6 +18,8 @@ import static org.prebid.server.functional.model.bidder.BidderName.GENERIC class StoredResponseSpec extends BaseSpec { + private final PrebidServerService pbsService = pbsServiceFactory.getService(["cache.default-ttl-seconds.banner": ""]) + @PendingFeature def "PBS should not fail auction with storedAuctionResponse when request bidder params doesn't satisfy json-schema"() { given: "BidRequest with bad bidder datatype and storedAuctionResponse" @@ -33,7 +36,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should not contain errors and warnings" assert !response.ext?.errors @@ -56,7 +59,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored auction response" assert response.id == bidRequest.id @@ -82,7 +85,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored bid response" assert response.id == bidRequest.id @@ -111,7 +114,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored bid response and change bid.impId on imp.id" assert response.id == bidRequest.id @@ -140,7 +143,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain warning information" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] @@ -161,7 +164,7 @@ class StoredResponseSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid == [storedAuctionResponse] @@ -190,7 +193,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid == [storedAuctionResponse] @@ -214,7 +217,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid @@ -244,7 +247,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain warning information" assert response.ext?.warnings[ErrorType.PREBID]*.message.contains('SeatBid can\'t be null in stored response') @@ -257,10 +260,10 @@ class StoredResponseSpec extends BaseSpec { given: "Default basic BidRequest with stored response" def bidRequest = BidRequest.defaultBidRequest def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) - bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: storedAuctionResponse) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: storedAuctionResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] @@ -272,10 +275,10 @@ class StoredResponseSpec extends BaseSpec { def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty seatbid"() { given: "Default basic BidRequest with empty stored response" def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid()) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid()) when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS throws an exception" def exception = thrown(PrebidServerException) @@ -289,10 +292,10 @@ class StoredResponseSpec extends BaseSpec { def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty bids"() { given: "Default basic BidRequest with empty bids for stored response" def bidRequest = BidRequest.defaultBidRequest - bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid(bid: [], seat: GENERIC)) + bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid(bid: [], seat: GENERIC)) when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS throws an exception" def exception = thrown(PrebidServerException) @@ -313,7 +316,7 @@ class StoredResponseSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse] @@ -329,7 +332,7 @@ class StoredResponseSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response bids as requested" assert convertToComparableSeatBid(response.seatbid).bid.flatten().sort() == @@ -343,7 +346,7 @@ class StoredResponseSpec extends BaseSpec { given: "Default basic BidRequest with stored response" def bidRequest = BidRequest.defaultBidRequest def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) - bidRequest.tap{ + bidRequest.tap { imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap { seatBidObject = SeatBid.getStoredResponse(bidRequest) } @@ -351,7 +354,7 @@ class StoredResponseSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain same stored auction response as requested" assert response.seatbid == [storedAuctionResponse] diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 0e3d0220ba1..2f34799817c 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -113,6 +113,7 @@ import org.prebid.server.settings.model.AccountAuctionEventConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.VideoStoredDataResult; +import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; import org.prebid.server.vast.VastModifier; import java.math.BigDecimal; @@ -156,6 +157,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @@ -190,6 +192,8 @@ public class BidResponseCreatorTest extends VertxTest { private ActivityInfrastructure activityInfrastructure; @Mock(strictness = LENIENT) private CacheTtl mediaTypeCacheTtl; + @Mock(strictness = LENIENT) + private CacheDefaultTtlProperties cacheDefaultProperties; @Spy private WinningBidComparatorFactory winningBidComparatorFactory; @@ -209,6 +213,11 @@ public void setUp() { given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(null); given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(null); + given(cacheDefaultProperties.getBannerTtl()).willReturn(null); + given(cacheDefaultProperties.getVideoTtl()).willReturn(null); + given(cacheDefaultProperties.getAudioTtl()).willReturn(null); + given(cacheDefaultProperties.getNativeTtl()).willReturn(null); + given(categoryMappingService.createCategoryMapping(any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture( CategoryMappingResult.of(emptyMap(), emptyMap(), invocationOnMock.getArgument(0), null))); @@ -1640,7 +1649,8 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() { 20, clock, jacksonMapper, - mediaTypeCacheTtl); + mediaTypeCacheTtl, + cacheDefaultProperties); // when final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); @@ -3807,7 +3817,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { final Imp imp = Imp.builder().id("impId").exp(20).build(); final List bidderResponses = asList(BidderResponse.of( "bidder1", - givenSeatBid(BidderBid.of(bid, banner, "USD")), + givenSeatBid(BidderBid.of(bid, video, "USD")), 100)); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() @@ -3815,7 +3825,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(30) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -3825,7 +3835,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3834,6 +3845,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3855,6 +3871,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(10); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(10); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -3869,7 +3886,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { final Imp imp = Imp.builder().id("impId").exp(20).build(); final List bidderResponses = asList(BidderResponse.of( "bidder1", - givenSeatBid(BidderBid.of(bid, banner, "USD")), + givenSeatBid(BidderBid.of(bid, video, "USD")), 100)); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() @@ -3877,7 +3894,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(30) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -3887,7 +3904,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3896,6 +3914,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3917,6 +3940,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() { final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(20); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(20); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -3931,7 +3955,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { final Imp imp = Imp.builder().id("impId").exp(null).build(); final List bidderResponses = asList(BidderResponse.of( "bidder1", - givenSeatBid(BidderBid.of(bid, banner, "USD")), + givenSeatBid(BidderBid.of(bid, video, "USD")), 100)); final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() @@ -3939,7 +3963,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(30) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -3949,7 +3973,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -3958,6 +3983,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -3968,7 +3998,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { assertThat(response.succeeded()).isTrue(); assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) - .containsExactly(30); + .containsExactly(31); verify(coreCacheService).cacheBidsOpenrtb( bidsArgumentCaptor.capture(), @@ -3979,6 +4009,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(30); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(31); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -3987,7 +4018,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() { } @Test - public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtl() { + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtlForBannerBid() { // given final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); final Imp imp = Imp.builder().id("impId").exp(null).build(); @@ -4001,7 +4032,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(null) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(31) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -4011,7 +4042,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() - .bannerCacheTtl(60) + .bannerCacheTtl(40) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -4020,6 +4052,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -4030,7 +4067,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne assertThat(response.succeeded()).isTrue(); assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) - .containsExactly(60); + .containsExactly(40); verify(coreCacheService).cacheBidsOpenrtb( bidsArgumentCaptor.capture(), @@ -4040,7 +4077,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); - assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(60); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(40); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4049,7 +4087,76 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne } @Test - public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl() { + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountVideoTtlForVideoBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, video, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(41); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(41); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(41); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtlForBannerBid() { // given final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); final Imp imp = Imp.builder().id("impId").exp(null).build(); @@ -4063,7 +4170,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl .shouldCacheBids(true) .shouldCacheVideoBids(true) .cacheBidsTtl(null) - .cacheVideoBidsTtl(40) + .cacheVideoBidsTtl(null) .build(); final AuctionContext auctionContext = givenAuctionContext( @@ -4074,6 +4181,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl .id("accountId") .auction(AccountAuctionConfig.builder() .bannerCacheTtl(null) + .videoCacheTtl(41) .events(AccountEventsConfig.of(true)) .build()) .build())) @@ -4082,6 +4190,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl // just a stub to get through method call chain givenCacheServiceResult(singletonList(CacheInfo.empty())); given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); // when final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); @@ -4103,6 +4216,352 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(50); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtlForVideoBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, video, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(null) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(51); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(51); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(51); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForBannerBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, banner, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(null) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(null); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(60); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(60); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForVideoBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, video, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(null) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(null); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(61); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(61); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(61); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForAudioBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, audio, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(62); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(62); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); + assertThat(contextArgumentCaptor.getValue()) + .satisfies(context -> { + assertThat(context.isShouldCacheBids()).isTrue(); + assertThat(context.isShouldCacheVideoBids()).isTrue(); + }); + } + + @Test + public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForNativeBid() { + // given + final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build(); + final Imp imp = Imp.builder().id("impId").exp(null).build(); + final List bidderResponses = asList(BidderResponse.of( + "bidder1", + givenSeatBid(BidderBid.of(bid, xNative, "USD")), + 100)); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder() + .doCaching(true) + .shouldCacheBids(true) + .shouldCacheVideoBids(true) + .cacheBidsTtl(null) + .cacheVideoBidsTtl(null) + .build(); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .events(mapper.createObjectNode()) + .build())), imp), + builder -> builder.account(Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .bannerCacheTtl(40) + .videoCacheTtl(41) + .events(AccountEventsConfig.of(true)) + .build()) + .build())) + .with(toAuctionParticipant(bidderResponses)); + + // just a stub to get through method call chain + givenCacheServiceResult(singletonList(CacheInfo.empty())); + given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50); + given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51); + given(cacheDefaultProperties.getBannerTtl()).willReturn(60); + given(cacheDefaultProperties.getVideoTtl()).willReturn(61); + given(cacheDefaultProperties.getAudioTtl()).willReturn(62); + given(cacheDefaultProperties.getNativeTtl()).willReturn(63); + + // when + final Future response = target.create(auctionContext, cacheInfo, MULTI_BIDS); + + // then + final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class); + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + + assertThat(response.succeeded()).isTrue(); + assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp) + .containsExactly(63); + + verify(coreCacheService).cacheBidsOpenrtb( + bidsArgumentCaptor.capture(), + same(auctionContext), + contextArgumentCaptor.capture(), + any()); + + final List capturedBidInfo = bidsArgumentCaptor.getValue(); + assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); + assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(63); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull(); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4277,7 +4736,7 @@ public void createShouldSendCacheRequestWithVideoBidWithTtlMaxOfTtlAndVideoTtl() final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsExactly(30); - assertThat(capturedBidInfo).extracting(BidInfo::getVideoTtl).containsExactly(40); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsExactly(40); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4336,7 +4795,7 @@ public void createShouldSendCacheRequestWithBannerBidWithTtlMaxOfTtlAndVideoTtl( final List capturedBidInfo = bidsArgumentCaptor.getValue(); assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId"); assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsExactly(30); - assertThat(capturedBidInfo).extracting(BidInfo::getVideoTtl).containsOnlyNulls(); + assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnlyNulls(); assertThat(contextArgumentCaptor.getValue()) .satisfies(context -> { assertThat(context.isShouldCacheBids()).isTrue(); @@ -4576,7 +5035,8 @@ private BidResponseCreator givenBidResponseCreator(int truncateAttrChars) { truncateAttrChars, clock, jacksonMapper, - mediaTypeCacheTtl); + mediaTypeCacheTtl, + cacheDefaultProperties); } private static String toTargetingByKey(Bid bid, String targetingKey) { diff --git a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json index 4908b67e9c1..fe8eba5c934 100644 --- a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json +++ b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json @@ -27,7 +27,8 @@ "origbidcpm": 12.09 } }, - "aid":"tid" + "aid":"tid", + "ttlseconds": 300 }, { "type": "json", @@ -60,7 +61,8 @@ "origbidcur": "USD" } }, - "aid":"tid" + "aid":"tid", + "ttlseconds": 300 } ] } diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json index 9d49c702f5e..e6127bea4f0 100644 --- a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json +++ b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json @@ -11,6 +11,7 @@ "crid": "crid2", "w": 120, "h": 600, + "exp": 300, "ext": { "prebid": { "type": "banner", @@ -35,6 +36,7 @@ { "id": "31124", "impid": "impId-video-cache-update", + "exp": 1500, "price": 3, "adm": "adm1", "crid": "crid1", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json b/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json index f086c053112..b7a0ac4311d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json index c223e8f56d3..f6f8aa08087 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json @@ -6,6 +6,7 @@ { "id": "randomid", "impid": "test-imp-id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json index f80400fe5d1..b9b61a696c0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json index ba9ea7db56d..a0bab7d6cc2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json index ee0b96cb442..0c48f3a8431 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json index 320108794b5..f4417095fa3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 11.393, "adomain": [ ], @@ -22,6 +23,7 @@ { "id": "bid_id_banner", "impid": "imp_id_banner", + "exp": 300, "price": 11.393, "adomain": [], "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json index 05c116d4aa2..25ebe533dcc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json @@ -6,6 +6,7 @@ { "id": "id", "impid": "id", + "exp": 300, "price": 46.6, "adm": "", "crid": "Dummy_supership.jp", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json index 9895195c325..f5fd5214de2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 2.184, "adm": "

\"\"
", "crid": "demo-424", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json index 92b00eb8c2b..a6a8e2bd0c8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 2.25, "adm": "", "adid": "2002", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json index 9f868cc7baa..53df688de45 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json @@ -24,4 +24,4 @@ } ], "bidid": "bid_id" -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json index f1d38a7780f..9563c1ff672 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "adm021", "adid": "19005", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json index 8d448c61116..8884760961e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json @@ -6,6 +6,7 @@ { "id": "bid_id1", "impid": "imp_id1", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json index 6ad0d2f637f..0277bb7f78d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json index 5561b33da3b..aceadcc04ac 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json index 4352d750af3..75f33a522f6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json @@ -16,6 +16,7 @@ }, "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01 } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json index beffca0d359..61bac864a49 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json @@ -6,6 +6,7 @@ { "id": "some_ad_id", "impid": "imp_id", + "exp": 300, "price": 42420.00, "adm": "some_html", "adid": "some_ad_id", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json index 76c4005d49c..3f62c1fb7db 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json @@ -6,6 +6,7 @@ { "id": "adoceanmyaozpniqismex", "impid": "imp_id", + "exp": 300, "price": 10, "adm": " ", "crid": "0af345b42983cc4bc0", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json index ba80a545eed..4edc56ade74 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json @@ -26,4 +26,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json index db0b1aeaa49..7822b00cdbb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json index dcff8f22c64..b2c52bb7d9e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.16346, "adm": "some-test-ad", "crid": "crid001", @@ -36,4 +37,4 @@ "auctiontimestamp": 1626182712962 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json index 7f09ff7886b..1682aad2c48 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json index e24dfce9f48..3c4fc34a311 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 6.66, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json index 5b7e94562fd..073a812bcdb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json index 0d05040052c..1d301f75624 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json @@ -6,6 +6,7 @@ { "id": "22e26bd9a702bc1", "impid": "22e26bd9a702bc", + "exp": 300, "price": 1.090, "adm": "Tag_Example", "adomain": [ diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json index d480aae971a..e1341dbdcca 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adid": "adid001", "cid": "cid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json index b9d85d4e632..855d418643f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json index 03c5ee91218..d5b04833e91 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json @@ -17,4 +17,4 @@ "group": 0 } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json index 23465125a4e..809b063e228 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json index 15d06b7c923..1da8f18279d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json @@ -17,4 +17,4 @@ "group": 0 } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json index 458b300cb66..b73512ad65c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json index e6795976a7f..c5bfdb6d592 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "mtype": 1, "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json index 60c2ad42bab..d786080f717 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "h": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json index df8ec148aa1..92ba70c5c42 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json index eccc7f38dec..a6c118e0913 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json index 81b8aa40e9e..0961b1f67ec 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json index e291739474c..a4c0edc3e09 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json index ec08af30179..96aa18de5d8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json index 1dff571757c..f5ee4e08e9c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "mtype": 1, "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json index d2f3908c4b3..413a3ffe241 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json index 5b8ce2dfb32..7010f75f5e6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 10, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json index e291739474c..a4c0edc3e09 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json @@ -17,4 +17,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json index f3b649ebbec..b61aebbccd6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json index ca0b59e06b3..b8ccdb2d3b3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json index dc3186c5778..ec393ab7ca0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json index e9c1f602280..2122bfc623a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json b/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json index da99c54e188..15ae12d04c5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json @@ -4,19 +4,22 @@ "type": "xml", "value": "some-test-ad-3", "aid": "bid_id", - "key": "2.0_IAB10-1_0s_{{uuid}}" + "key": "2.0_IAB10-1_0s_{{uuid}}", + "ttlseconds": 1500 }, { "type": "xml", "value": "some-test-ad", "aid": "bid_id", - "key": "5.5_IAB20-3_0s_{{uuid}}" + "key": "5.5_IAB20-3_0s_{{uuid}}", + "ttlseconds": 1500 }, { "type": "xml", "value": "some-test-ad-2", "aid": "bid_id", - "key": "2.5_IAB18-5_0s_{{uuid}}" + "key": "2.5_IAB18-5_0s_{{uuid}}", + "ttlseconds": 1500 } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json b/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json index 4a71755b12d..f673f770ad2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json index cef76b6cec9..6b9a19e7187 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6_4.7", "nurl": "nurl_4.7", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json b/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json index 376d88dbf44..e1d18a2ecb5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 9.0, "adm": "{\"bid_id\":\"10\"}", "adid": "10", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json index 64383cd93de..c4c971e466e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json index cc260a44b94..6fca036d997 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 0.5, "adm": "some-test-ad", "adid": "29681110", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json index 37c3691752c..676eb7d802a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json @@ -6,6 +6,7 @@ { "id": "bid_id1", "impid": "imp_id1", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json index 31a15adc1f9..e0e02fc7381 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json index 1ca1cb7607c..c591ef97cfd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6_4.7", "nurl": "nurl_4.7", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json index 5338cc8c4d4..5e10a21b5de 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json @@ -6,6 +6,7 @@ { "id": "imp_idBanner", "impid": "imp_id", + "exp": 300, "price": 2.942807912826538, "adm": "
", "crid": "crid_3", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json index 9419a85783d..bed5b0e3939 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "bid_id", + "exp": 300, "price": 2.942808, "adid": "94395500", "crid": "94395500", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json index 4b0e7de3f44..1d2bac9f898 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6", "crid": "crid6", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json b/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json index ed208dd5654..cf2a6e0d031 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json index 605deba4cb1..e63089babb2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json index 80cfe99af9e..fdba12487c7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6_4.7", "nurl": "nurl_4.7", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json index eb4e503494f..0ea56280b80 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json index a45f9eeb3c9..ba0b73cfaf1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json index ba657da0438..399a568d088 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json index 8cb8a61c009..8c8b0128e98 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json index 523bdbbcda4..9428b19fd0a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adid": "adid001", "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json index 287c05c61aa..013cd170712 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 3.33, "adm": "adm001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json index 9bf200e6d9d..22a67229971 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json index de26e4363ea..f4ddf9222af 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json index 939897d89a1..71412aa8e6c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json index 1d36233fd2d..cbbae431606 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json b/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json index e9242e76699..4d5d8f5ce9e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json b/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json index 39819c16e18..43de8398d78 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json index 6029a55596f..6107fe586a3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "mtype": 1, "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json index c5b97cc0914..f2f43ba2c1d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json @@ -6,6 +6,7 @@ { "id": "imp_id", "impid": "imp_id", + "exp": 300, "price": 2.942808, "adm": "
", "adid": "94395500", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json index 28eb0bc9e9d..7fcafb472c7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json index 4837ee79517..06fadbf0b09 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json b/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json index cbad770f0e5..9f59c942b28 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 6.66, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json index 3491c77189e..f551e12bbe4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json index c4d521102b5..d7914b5fb58 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json b/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json index 0da511e197e..20f86d6a348 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json b/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json index aadc8da3482..666a177b9c1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 3.33, "adm": "adm001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json index 9aa7d077c16..d24913feeef 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adm": "hi", "cid": "test_cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json index 1290f6775fa..a28d27cf14a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json @@ -7,6 +7,7 @@ { "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", "impid": "test-imp-id", + "exp": 300, "price": 0.500000, "adm": "some-test-ad", "crid": "crid_10", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json index 464f7df6b6a..3ac3ccb1672 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json index fb24eb9368c..52b36682c8a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json index 9444279b4a6..6a685a06ba4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json index fb1c7803346..9d7e6b1ca5b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json index 7ce6a9dd1ae..a73407d0bb8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.77, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json index dbbff620bf4..6b058b84507 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json index 8f1dfac20f0..223095836df 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json index d24a92228d1..34d7cd3fbf2 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 11.393, "adomain": [ ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json index 40de103c524..0725b710372 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id_banner", "impid": "imp_id_banner", + "exp": 300, "price": 11.393, "adomain": [], "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json index 8c5b4ea599e..ad84acca12c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json index c34b9f61946..e7b10ebdda4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "cid": "test_cid", "crid": "test_banner_crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json index 81da7b92978..d28852c4ccd 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json index 0c6bd826b02..b2442775de6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "mtype": 1, "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json b/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json index ce119ca7efd..2b9d8f83ca4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json index 5de88ba03df..930aa7c8b06 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json index c71dd2560c2..3795c5a9c62 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json index 6d2e3cf823c..552b0f3a934 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json @@ -6,6 +6,7 @@ { "id": "imp_id", "impid": "imp_id", + "exp": 300, "price": 2.942808, "adm": "
", "adid": "94395500", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json index b3b8e50e406..ce65aba9a9b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "
test
", "adid": "imp_id", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json index 16a9acaefd1..f10ab8e286e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json index aadd13302aa..3dd393badc9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 5.0, "adm": "adm4", "crid": "crid4", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json index 8cb45ddcbac..4aa8c6d0985 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 6.0, "adm": "adm4", "crid": "crid4", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json index 0aa7a90e2d4..7f2babf609e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json b/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json index 1d694702c90..d0043247edf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json index ae5c74865aa..0b63bd03f57 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json index 4a1aac1a8c5..e2c3508d1ab 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json b/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json index 4246d1a5016..e055b56bb65 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 11.393, "adm": "some adm price 10", "adomain": [ diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json index a6d946f403d..da840110dcf 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json @@ -6,6 +6,7 @@ { "id": "183599115", "impid": "imp_id", + "exp": 300, "price": 12.34, "adm": "creativeContent", "crid": "81325690", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json index 834a4af5e9d..d583e32cc4f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json @@ -6,6 +6,7 @@ { "id": "12345_freewheelssp-test_1", "impid": "imp-1", + "exp": 1500, "price": 1.0, "adid": "7857", "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json index fb69a968abd..683c42863d0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json index ca56cb19b7e..ce0e41775aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "29681110", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json index c88d409c2c2..4d6ea3f9060 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json index 808b06e512e..8f2d2e4407c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json index 552408995c8..4ee1ff6a6c8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json @@ -6,6 +6,7 @@ { "id": "bid001", "impid": "impId001", + "exp": 1500, "price": 2.997, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json index 1b5c1802325..a6d65dfcae0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json @@ -36,7 +36,8 @@ "origbidcpm": 3.33 } }, - "aid": "tid" + "aid": "tid", + "ttlseconds" : 1500 } ] } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json b/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json index 7ab5cf7f347..505a3ca4b5c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json index 4554400f4b1..728dccb2b13 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json b/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json index df8a43a904f..7d19f2a3f5c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json index a71711d597d..f6a034ca8a3 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json index 682e43e4516..7c131a1f180 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json index 95248ad02c0..2068eb254c5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json index c576adccb02..931449bc021 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json index c576adccb02..931449bc021 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json index c576adccb02..931449bc021 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json index 1ad60895c93..3f3b9084e45 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json index 3e1b8422400..05a00845f55 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 300, "w": 250, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json index 2b08c0e9f75..dee5e9c6ebb 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58025103", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 250, "w": 300, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json index 621a43422cf..2fcaffd7d5b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 300, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 250, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json index 033288ceb0e..d6d29d2c614 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 250, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 300, "nurl":"" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json index aa5fa9e4250..78e93b06659 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json index dde9fbcb4ea..3da566123a6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 1280, "w": 720, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json index 22f804c5841..8253f6b3e62 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 350, "w": 400, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json index 898142d244c..b67ae7292e1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58022259", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "h": 350, "w": 400, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json index a32f2cdc011..928b8e37d12 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 1280, "w": 720, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json index 048960f1312..c59dbe20672 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json @@ -29,6 +29,7 @@ "h": 300, "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 0.404, "w": 250, "nurl": "" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json index 84bfd612d2e..2d7403f3e7e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json index 84bfd612d2e..2d7403f3e7e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json index edf5ff6ca1e..72686361101 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json index aa9c0cccf38..9425d23926c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 500, "w": 600, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json index dde2af86099..16f6247161f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json @@ -21,6 +21,7 @@ "crid": "58001445", "id": "test-imp-id", "impid": "test-imp-id", + "exp": 1500, "price": 0.404, "h": 1280, "w": 720, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json index e5e6af0ffa2..e37c977df53 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json b/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json index 42b87323685..8eae0a3d518 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.77, "adm": "adm001", "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json b/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json index 4bfd0bcee03..9ea7cb8766f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json index 2ed506e5a4c..a826a40c221 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json b/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json index c561c6a98e8..6361cafe796 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json b/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json index d5d69df907e..55bd555ed6b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json index ea2a8cebd9e..d2d1bd207fa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json b/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json index 174c7a0894b..3100814d919 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 10, "adomain": [ ], @@ -33,4 +34,4 @@ "auctiontimestamp": 0 } } -} \ No newline at end of file +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json b/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json index 9255d5d9323..9a0fc145939 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json index c07c5d21b49..4322850fa9d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.3, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json index 76ef74b808d..3fc5c87e169 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json index 4a6e48aac57..b73fb55b074 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json index 5be6fc4788c..1ecdc0136a1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.7, "adm": "adm6", "crid": "crid6", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json index 48fed77c3e7..31a01fdf368 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json index f9c2484affa..196ed6b59e7 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json index 10481c0accb..2589f6a4a4d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json index 6b513b46072..aaccad9be2d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json index 668b51a85a8..5cec9fef037 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json index 046aeaa3005..07da2713c33 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json index 25228273c75..56f25641c69 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json index a21706f9abf..59dc0706b2d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json b/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json index 999b4184dbd..e90d9b5f6aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adid": "adid001", "adm": "", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json index 409ecdcd328..5bf9bad0853 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json index 1036e1e84d0..200e13238f8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json index 0433389b511..d1ae959bebe 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.35, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json b/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json index 5079a616a00..f5503cf319d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json index dd7aa14d18c..4319787685b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json index 2c86435fb5b..30397fe4de4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json b/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json index 80cbbdf2ebb..58b5b47ee41 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json index a7c3e9ba1e2..194301eae99 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json index dacba278abe..28e33d7e084 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json @@ -6,6 +6,7 @@ { "id": "test-imp-id", "impid": "test-imp-id", + "exp": 300, "price": 2.734, "adm": "", "adomain": [ diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json index c7c4540bd47..1c31dddaea0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json index fd8cbf0a699..111e147d710 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json index dad8c29bdf5..a6bccc7525a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json index 8bd2fdc004e..b2536b23494 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 7.35, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json index 5c5f50670a2..7eb299b660f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json index 14ebb3b62f8..55b8dba2496 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json @@ -6,6 +6,7 @@ { "id": "randomid", "impid": "test-imp-id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json index 42c6696f9bb..a810ba066a1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json index b37ebc3ebea..82cc50b31be 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json index 89dc1790afe..f2c474641aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.5, "nurl": "nurl", "adm": "some-test-ad", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json index 217cac91e56..d24c8274ed9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json index 898532edc4b..10cdbd0a6f5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "banner_imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "29681110", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json index d76e9f071e5..28f9caabf7e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json @@ -6,6 +6,7 @@ { "id": "request_id", "impid": "imp_id", + "exp": 300, "price": 10.2, "adm": "adm", "crid": "id", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json index 1c1fbe7791b..fb8a20ae2b0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "", "cid": "test_cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json index d4ff118ad3c..3f22980426a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adm": "hi", "cid": "test_cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json b/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json index 17ba2b420b7..fba432c5be4 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json index bfd9600aa75..1e4f6f4a489 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json @@ -11,7 +11,7 @@ "crid": "crid1", "w": 300, "h": 250, - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", @@ -68,7 +68,7 @@ "crid": "crid1", "w": 300, "h": 250, - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", @@ -128,7 +128,7 @@ "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", "cid": "958", "crid": "29681110", - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", @@ -179,7 +179,7 @@ "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", "cid": "958", "crid": "69595837", - "exp": 120, + "exp": 1500, "ext": { "prebid": { "type": "video", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json index 2d951abdbab..770ee7c9d39 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json @@ -32,7 +32,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=21521324&a=5001&aid=tid&ts=1000&bidder=generic&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "json", @@ -68,7 +69,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=genericAlias&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "json", @@ -102,7 +104,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=generic&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "json", @@ -138,7 +141,8 @@ }, "wurl": "http://localhost:8080/event?t=win&b=222214214214&a=5001&aid=tid&ts=1000&bidder=genericAlias&f=i&int=" }, - "aid": "tid" + "aid": "tid", + "ttlseconds": 1500 }, { "type": "xml", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json index 71068f2cb94..be5cf8b9277 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "mtype": 1, "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json index fc7edb8ce86..ebeea62aba0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json index f6a94e868a8..315114b4f75 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "ext": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json b/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json index 80373e03cb7..4496bc2e4b6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "adid", "cid": "cid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json index 53761fd9c76..a996e780237 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 5.78, "adm": "adm00", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json index 8ffbc819833..0c22b5e89b9 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 5.78, "adm": "adm00", "crid": "crid00", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json index 2c7b4e4f22c..91f9dc59e5c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json index 6871f609875..4bbd63fe482 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json index 1a260bfa518..40a02aff7f5 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json @@ -16,6 +16,7 @@ }, "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "mtype": 1 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json b/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json index 6c19a2a44b7..cb7fbc34fe8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json index 38e6c451540..d7a74ef6d6d 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "mtype": 1, "price": 3.33, "adm": "adm001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json index 7a0cf377706..b0995267c08 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 1.25, "adm": "adm001", "crid": "crid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json index 79f17402a81..82e6a8182df 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json index f2c7121be84..303e8a7826c 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.01, "adid": "2068416", "cid": "8048", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json index 1c26c09c8c7..8bfc8b3f515 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json b/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json index 2b363db2f29..c26bf519990 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "some-test-ad", "adid": "12345678", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json index 53731b03bf0..4df8cf0c723 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "test-imp-id", + "exp": 1500, "price": 4.75, "adm": "adm9", "crid": "crid9", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json index 7b7334c52fe..d263e2b1f43 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json index a49d1f99e75..752b5519b40 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json index 332e1cae3ac..88c15aebf71 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 4.75, "adm": "adm8", "crid": "crid8", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json index 0f38f603a90..b6c810ede52 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 8.43, "adm": "adm14", "crid": "crid14", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json index 4ebd8ee119a..16db7e67c68 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 1.25, "adm": "adm001", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json b/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json index 101455149d7..06a8c3170aa 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "crid": "creativeId", "mtype": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json index 9e1e479b4ed..d3531b49cca 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 1500, "price": 5.78, "adm": "adm", "crid": "crid", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json index c3de29e49d4..f595ea39c9f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 3.33, "adm": "adm001", "adid": "adid001", diff --git a/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json index a2acc3d4df5..4e6047c3738 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json @@ -6,6 +6,7 @@ { "id": "bid_id", "impid": "imp_id", + "exp": 300, "price": 0.5, "adm": "