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 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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