From b0fde7760a2c51ed691606c0736fe31e45270d85 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 20 May 2025 15:07:23 +0300 Subject: [PATCH 01/51] Revert "Zeta Global SSP bidder port (#3966) --- .../zeta_global_ssp/ZetaGlobalSspBidder.java | 141 ----------- .../zeta_global_ssp/ExtImpZetaGlobalSSP.java | 9 - .../bidder/ZetaGlobalSspConfiguration.java | 43 ---- src/main/resources/bidder-config/generic.yaml | 21 ++ .../bidder-config/zeta_global_ssp.yaml | 24 -- .../static/bidder-params/zeta_global_ssp.json | 13 +- .../ZetaGlobalSspBidderTest.java | 221 ------------------ .../test-auction-zeta_global_ssp-request.json | 4 +- .../test-zeta_global_ssp-bid-request.json | 6 +- .../test-zeta_global_ssp-bid-response.json | 10 +- .../server/it/test-application.properties | 4 +- 11 files changed, 36 insertions(+), 460 deletions(-) delete mode 100644 src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java delete mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java delete mode 100644 src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java delete mode 100644 src/main/resources/bidder-config/zeta_global_ssp.yaml delete mode 100644 src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java diff --git a/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java b/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java deleted file mode 100644 index 85454c43f2d..00000000000 --- a/src/main/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidder.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.prebid.server.bidder.zeta_global_ssp; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import org.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.zeta_global_ssp.ExtImpZetaGlobalSSP; -import org.prebid.server.proto.openrtb.ext.response.BidType; -import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; -import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.HttpUtil; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class ZetaGlobalSspBidder implements Bidder { - - private static final TypeReference> ZETA_GLOBAL_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; - - private static final TypeReference> EXT_BID_TYPE_REFERENCE = - new TypeReference<>() { - }; - private static final String SID_MACRO = "{{AccountID}}"; - - private final String endpointUrl; - private final JacksonMapper mapper; - - public ZetaGlobalSspBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(endpointUrl); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest request) { - final Imp firstImp = request.getImp().getFirst(); - final ExtImpZetaGlobalSSP extImp; - - try { - extImp = parseImpExt(firstImp); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - - final HttpRequest httpRequest = BidderUtil.defaultRequest( - removeImpsExt(request), - resolveEndpoint(extImp), - mapper); - - return Result.withValues(Collections.singletonList(httpRequest)); - } - - private ExtImpZetaGlobalSSP parseImpExt(Imp imp) { - try { - return mapper.mapper().convertValue(imp.getExt(), ZETA_GLOBAL_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); - } - } - - private String resolveEndpoint(ExtImpZetaGlobalSSP extImpZetaGlobalSSP) { - return endpointUrl - .replace(SID_MACRO, Objects.toString(extImpZetaGlobalSSP.getSid(), "0")); - } - - private BidRequest removeImpsExt(BidRequest request) { - final List imps = new ArrayList<>(request.getImp()); - final Imp firstImp = imps.getFirst().toBuilder().ext(null).build(); - imps.set(0, firstImp); - - return request.toBuilder() - .imp(imps) - .build(); - } - - @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - final List errors = new ArrayList<>(); - return Result.of(extractBids(bidResponse, errors), errors); - } catch (DecodeException | PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private List extractBids(BidResponse bidResponse, List errors) { - if (bidResponse == null || bidResponse.getSeatbid() == null) { - return Collections.emptyList(); - } - return bidsFromResponse(bidResponse, errors); - } - - private List bidsFromResponse(BidResponse bidResponse, List errors) { - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) - .filter(Objects::nonNull) - .toList(); - } - - private BidderBid makeBid(Bid bid, String currency, List errors) { - final BidType mediaType = getMediaType(bid, errors); - return mediaType == null ? null : BidderBid.of(bid, mediaType, currency); - } - - private BidType getMediaType(Bid bid, List errors) { - try { - return Optional.ofNullable(bid.getExt()) - .map(ext -> mapper.mapper().convertValue(ext, EXT_BID_TYPE_REFERENCE)) - .map(ExtPrebid::getPrebid) - .map(ExtBidPrebid::getType) - .orElseThrow(IllegalArgumentException::new); - } catch (IllegalArgumentException e) { - errors.add(BidderError.badServerResponse( - "Failed to parse impression \"%s\" mediatype".formatted(bid.getImpid()))); - return null; - } - } -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java deleted file mode 100644 index b904d2e677a..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeta_global_ssp/ExtImpZetaGlobalSSP.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class ExtImpZetaGlobalSSP { - - Integer sid; -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java deleted file mode 100644 index aa98e645aa0..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/ZetaGlobalSspConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.prebid.server.spring.config.bidder; - -import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.zeta_global_ssp.ZetaGlobalSspBidder; -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.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -import jakarta.validation.constraints.NotBlank; - -@Configuration -@PropertySource(value = "classpath:/bidder-config/zeta_global_ssp.yaml", factory = YamlPropertySourceFactory.class) -public class ZetaGlobalSspConfiguration { - - private static final String BIDDER_NAME = "zeta_global_ssp"; - - @Bean("zetaglobalsspConfigurationProperties") - @ConfigurationProperties("adapters.zetaglobalssp") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps zetaGlobalSspBidderDeps(@Qualifier("zetaglobalsspConfigurationProperties") - BidderConfigurationProperties zetaGlobalSspConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(zetaGlobalSspConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new ZetaGlobalSspBidder(config.getEndpoint(), mapper)) - .assemble(); - } -} diff --git a/src/main/resources/bidder-config/generic.yaml b/src/main/resources/bidder-config/generic.yaml index a6522be1122..4aa01b82bbb 100644 --- a/src/main/resources/bidder-config/generic.yaml +++ b/src/main/resources/bidder-config/generic.yaml @@ -41,6 +41,27 @@ adapters: - video supported-vendors: vendor-id: 0 + zeta_global_ssp: + enabled: false + endpoint: https://ssp.disqus.com/bid/prebid-server?sid=GET_SID_FROM_ZETA + endpoint-compression: gzip + meta-info: + maintainer-email: DL-Zeta-SSP@zetaglobal.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 833 + usersync: + enabled: true + cookie-family-name: zeta_global_ssp + redirect: + url: https://ssp.disqus.com/redirectuser?sid=GET_SID_FROM_ZETA&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} + uid-macro: 'BUYERUID' + support-cors: false blue: enabled: false endpoint: https://prebid-us-east-1.getblue.io/?src=prebid diff --git a/src/main/resources/bidder-config/zeta_global_ssp.yaml b/src/main/resources/bidder-config/zeta_global_ssp.yaml deleted file mode 100644 index 4c050049d2e..00000000000 --- a/src/main/resources/bidder-config/zeta_global_ssp.yaml +++ /dev/null @@ -1,24 +0,0 @@ -adapters: - zeta_global_ssp: - endpoint: https://ssp.disqus.com/bid/prebid-server?sid={{AccountID}} - endpoint-compression: gzip - geoscope: - - global - modifying-vast-xml-allowed: true - meta-info: - maintainer-email: DL-Zeta-SSP@zetaglobal.com - app-media-types: - - banner - - video - - audio - site-media-types: - - banner - - video - - audio - vendor-id: 833 - usersync: - cookie-family-name: zeta_global_ssp - redirect: - url: https://ssp.disqus.com/redirectuser?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} - support-cors: false - uid-macro: 'BUYERUID' diff --git a/src/main/resources/static/bidder-params/zeta_global_ssp.json b/src/main/resources/static/bidder-params/zeta_global_ssp.json index 8a6d1d0a060..91ff05ed089 100644 --- a/src/main/resources/static/bidder-params/zeta_global_ssp.json +++ b/src/main/resources/static/bidder-params/zeta_global_ssp.json @@ -1,13 +1,10 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Zeta Global SSP Adapter Params", - "description": "A schema which validates params accepted by the Zeta Global SSP adapter", - + "description": "A schema which validates params accepted by the Zeta SSP adapter", "type": "object", - "properties": { - "sid": { - "type": "integer", - "description": "An ID which identifies the publisher" - } - } + + "properties": {}, + + "required": [] } diff --git a/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java b/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java deleted file mode 100644 index 886af999d74..00000000000 --- a/src/test/java/org/prebid/server/bidder/zeta_global_ssp/ZetaGlobalSspBidderTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package org.prebid.server.bidder.zeta_global_ssp; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import org.junit.jupiter.api.Test; -import org.prebid.server.VertxTest; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderCall; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpRequest; -import org.prebid.server.bidder.model.HttpResponse; -import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.theadx.TheadxBidder; -import org.prebid.server.proto.openrtb.ext.ExtPrebid; -import org.prebid.server.proto.openrtb.ext.request.zeta_global_ssp.ExtImpZetaGlobalSSP; -import org.prebid.server.proto.openrtb.ext.response.BidType; - -import java.util.List; -import java.util.function.UnaryOperator; - -import static java.util.Collections.singletonList; -import static java.util.function.UnaryOperator.identity; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.prebid.server.bidder.model.BidderError.Type.bad_server_response; -import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; -import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; -import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; -import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; - -public class ZetaGlobalSspBidderTest extends VertxTest { - - private static final String ENDPOINT_URL = "https://test-url.com/{{AccountID}}"; - - private final ZetaGlobalSspBidder target = new ZetaGlobalSspBidder(ENDPOINT_URL, jacksonMapper); - - @Test - public void shouldFailOnBidderCreation() { - assertThatIllegalArgumentException().isThrownBy(() -> new TheadxBidder("invalid_url", jacksonMapper)); - } - - @Test - public void makeHttpRequestsShouldReturnErrorIfImpExtMissing() { - // given - final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.id("imp1") - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getMessage()).contains("Missing bidder ext in impression with id: imp1"); - assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); - }); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldCreateSingleRequestAndRemoveImpExt() { - // given - final Imp imp1 = givenImp(imp -> imp.id("imp1").ext(givenImpExt(11))); - final Imp imp2 = givenImp(imp -> imp.id("imp2").ext(givenImpExt(44))); - final BidRequest bidRequest = givenBidRequest(imp1, imp2); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - final HttpRequest httpRequest = result.getValue().getFirst(); - - // then - assertThat(result.getValue()) - .extracting(HttpRequest::getPayload) - .flatExtracting(BidRequest::getImp) - .extracting(Imp::getExt) - .containsExactly(null, givenImpExt(44)); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldReturnExpectedHeaders() { - // given - final BidRequest bidRequest = givenBidRequest(givenImp(identity())); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).hasSize(1).first() - .extracting(HttpRequest::getHeaders) - .satisfies(headers -> { - assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); - assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); - }); - assertThat(result.getErrors()).isEmpty(); - } - - @Test - public void makeHttpRequestsShouldResolveMacroInEndpointUrl() { - // given - final Imp imp1 = givenImp(imp -> imp.id("imp1").ext(givenImpExt(11))); - final BidRequest bidRequest = givenBidRequest(imp1); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getValue()).hasSize(1) - .extracting(HttpRequest::getUri) - .containsExactly("https://test-url.com/11"); - } - - @Test - public void makeBidsShouldReturnErrorIfResponseBodyInvalid() { - // given - final BidderCall httpCall = givenHttpCall("invalid-response-body"); - - // when - final Result> result = target.makeBids(httpCall, null); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getMessage()).contains("Failed to decode:"); - assertThat(error.getType()).isEqualTo(bad_server_response); - }); - } - - @Test - public void makeBidsShouldReturnEmptyListIfSeatBidIsNullOrEmpty() throws JsonProcessingException { - // given - final BidderCall httpCall = - givenHttpCall(mapper.writeValueAsString(BidResponse.builder().cur("USD").build())); - - // when - final Result> result = target.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeBidsShouldReturnErrorIfCannotResolveBidType() throws JsonProcessingException { - // given - final Bid bid = givenBid("imp1", mapper.createObjectNode()); - final BidderCall httpCall = - givenHttpCall(mapper.writeValueAsString(givenBidResponse(List.of(bid)))); - - // when - final Result> result = target.makeBids(httpCall, null); - - // then - assertThat(result.getValue()).isEmpty(); - assertThat(result.getErrors()).hasSize(1) - .allSatisfy(error -> { - assertThat(error.getMessage()).contains("Failed to parse impression \"imp1\" mediatype"); - assertThat(error.getType()).isEqualTo(bad_server_response); - }); - } - - @Test - public void makeBidsShouldReturnBannerBidIfTypeParsedProperly() throws JsonProcessingException { - // given - final ObjectNode extWithPrebidType = mapper.createObjectNode(); - extWithPrebidType.putObject("prebid").put("type", "banner"); - final Bid validBid = givenBid("imp1", extWithPrebidType); - - final BidResponse bidResponse = givenBidResponse(List.of(validBid)); - final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); - - // when - final Result> result = target.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); - final BidderBid bidderBid = result.getValue().getFirst(); - assertThat(bidderBid.getBid().getImpid()).isEqualTo("imp1"); - assertThat(bidderBid.getType()).isEqualTo(BidType.banner); - assertThat(bidderBid.getBidCurrency()).isEqualTo("USD"); - } - - private static BidRequest givenBidRequest(Imp... imps) { - return BidRequest.builder().imp(List.of(imps)).build(); - } - - private static Imp givenImp(UnaryOperator impCustomizer) { - return impCustomizer.apply(Imp.builder().id("imp_id").ext(givenImpExt(11))).build(); - } - - private static ObjectNode givenImpExt(Integer sid) { - return mapper.valueToTree(ExtPrebid.of(null, ExtImpZetaGlobalSSP.of(sid))); - } - - private static BidderCall givenHttpCall(String body) { - return BidderCall.succeededHttp( - HttpRequest.builder().payload(null).build(), - HttpResponse.of(200, null, body), - null); - } - - private static Bid givenBid(String impId, ObjectNode ext) { - return Bid.builder().impid(impId).ext(ext).build(); - } - - private static BidResponse givenBidResponse(List bids) { - return BidResponse.builder() - .cur("USD") - .seatbid(singletonList(SeatBid.builder().bid(bids).build())) - .build(); - } - -} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json index e68be13ed77..0a3824d6e31 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-request.json @@ -12,9 +12,7 @@ ] }, "ext": { - "zeta_global_ssp": { - "sid": 11 - } + "zeta_global_ssp": {} } } ], diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json index d982ec42345..2608812c09e 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-request.json @@ -11,7 +11,11 @@ } ] }, - "secure": 1 + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": {} + } } ], "site": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json index 39d74ae42cd..c31fabcb822 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-zeta_global_ssp-bid-response.json @@ -12,15 +12,9 @@ "cid": "cid001", "adm": "adm001", "h": 250, - "w": 300, - "ext": { - "prebid": { - "type": "banner" - } - } + "w": 300 } - ], - "seat": "zeta_global_ssp" + ] } ] } 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 a19968c87c2..450ae419cb5 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -14,6 +14,8 @@ adapters.generic.aliases.cwire.enabled=true adapters.generic.aliases.cwire.endpoint=http://localhost:8090/cwire-exchange adapters.generic.aliases.infytv.enabled=true adapters.generic.aliases.infytv.endpoint=http://localhost:8090/infytv-exchange +adapters.generic.aliases.zeta_global_ssp.enabled=true +adapters.generic.aliases.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange adapters.aceex.enabled=true adapters.aceex.endpoint=http://localhost:8090/aceex-exchange adapters.acuityads.enabled=true @@ -579,8 +581,6 @@ adapters.yieldone.enabled=true adapters.yieldone.endpoint=http://localhost:8090/yieldone-exchange adapters.zeroclickfraud.enabled=true adapters.zeroclickfraud.endpoint=http://{{Host}}/zeroclickfraud-exchange?sid={{SourceId}} -adapters.zeta_global_ssp.enabled=true -adapters.zeta_global_ssp.endpoint=http://localhost:8090/zeta_global_ssp-exchange adapters.aax.enabled=true adapters.aax.endpoint=http://localhost:8090/aax-exchange adapters.zmaticoo.enabled=true From d2c03573170e0dc48cde6b7d8bf7e0e43899bb2c Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 20 May 2025 15:07:35 +0300 Subject: [PATCH 02/51] Tests: Fix invalid functional tests (#3967) --- .../pricefloors/PriceFloorsRulesSpec.groovy | 19 ++++++++++++------- .../privacy/GppSyncUserActivitiesSpec.groovy | 2 +- .../GppTransmitEidsActivitiesSpec.groovy | 6 ++---- .../GppTransmitUfpdActivitiesSpec.groovy | 6 ++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy index 96247fe0157..c5404290135 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy @@ -8,8 +8,8 @@ import org.prebid.server.functional.model.config.AlternateBidderCodes import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.pricefloors.Country -import org.prebid.server.functional.model.pricefloors.MediaType import org.prebid.server.functional.model.pricefloors.FloorModelGroup +import org.prebid.server.functional.model.pricefloors.MediaType import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule @@ -29,6 +29,8 @@ import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils +import java.time.Instant + import static org.prebid.server.functional.model.ChannelType.WEB import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.AMX @@ -61,7 +63,6 @@ import static org.prebid.server.functional.model.request.auction.FetchStatus.ERR import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Prebid.Channel import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR -import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { @@ -177,7 +178,10 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { } def "PBS should consider rules file invalid when rules file contains an unrecognized dimension in the schema"() { - given: "BidRequest with domain" + given: "Test start time" + def startTime = Instant.now() + + and: "BidRequest with domain" def domain = PBSUtils.randomString def accountId = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -218,14 +222,15 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { assert bidderRequest.ext?.prebid?.floors?.location == NO_DATA assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR - and: "PBS should not contain errors" + and: "PBS should not contain errors or warnings" assert !response.ext?.errors + assert !response.ext?.warnings and: "PBS should log a warning" - assert response.ext?.warnings[PREBID]*.code == [999] - assert response.ext?.warnings[PREBID]*.message.first.contains("Cannot deserialize value of type " + + def logs = floorsPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "Cannot deserialize value of type " + "`org.prebid.server.floors.model.PriceFloorField` " + - "from String \"bogus\": not one of the values accepted for Enum class") + "from String \"bogus\": not one of the values accepted for Enum class").size() == 1 and: "PBS should not reject the entire auction" assert !response.seatbid.isEmpty() diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy index baac79520c3..2303bfc9e5f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy @@ -568,7 +568,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 and: "Should add a warning when in debug mode" - assert response.warnings == ["GPP string invalid: Unable to decode '$invalidGpp'"] + assert response.warnings.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) } def "PBS cookie sync call when request have different gpp consent but match and rejecting should exclude bidders URLs"() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy index 3d595ab0612..41e1079986d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitEidsActivitiesSpec.groovy @@ -928,8 +928,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source and: "Should add a warning when in debug mode" - assert response.ext.warnings[PREBID]?.code == [999] - assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) and: "Response should not contain any errors" assert !response.ext.errors @@ -2065,8 +2064,7 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 and: "Should add a warning when in debug mode" - assert response.ext.warnings[PREBID]?.code == [999] - assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) and: "Response should contain consent_string errors" assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $invalidGpp"] diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy index dab901bd93d..25f18381131 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppTransmitUfpdActivitiesSpec.groovy @@ -1232,8 +1232,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { assert bidderRequest.user.eids == bidRequest.user.eids and: "Should add a warning when in debug mode" - assert response.ext.warnings[PREBID]?.code == [999] - assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) and: "Response should not contain any errors" assert !response.ext.errors @@ -2712,8 +2711,7 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 and: "Should add a warning when in debug mode" - assert response.ext.warnings[PREBID]?.code == [999] - assert response.ext.warnings[PREBID]?.message == ["GPP string invalid: Unable to decode '$invalidGpp'"] + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) and: "Response should contain consent_string errors" assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $invalidGpp"] From 25587d67c8a7c77653ef346e9f2b26ca9c59f97c Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 20 May 2025 17:05:30 +0300 Subject: [PATCH 03/51] Revert "Dependencies: Change spring-boot version (#3954)" (#3968) --- extra/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/pom.xml b/extra/pom.xml index cf4343a8da4..cb3ec69cae6 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -33,7 +33,7 @@ 10.17.0 - 3.4.2 + 3.4.4 4.5.14 2.0.1.Final 4.4 From 777098810e530d5318d0975b539d28d7820bd9c2 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Tue, 20 May 2025 22:40:44 -0400 Subject: [PATCH 04/51] Prebid Server prepare release 3.26.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 6d61e280ec4..ab2c814cdcf 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.26.0-SNAPSHOT + 3.26.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 93c560669fe..24943ec7599 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index 29f5f96575a..e7ec68fb357 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index 54585080175..d21f13f4e8b 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 7435178a1d4..baf74fb1b70 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index 6115bf201d2..c179e532aee 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 02d312debf6..02db0f3a543 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 5fa580d91eb..d043e6fa2b7 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.26.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 51e717fa4de..75edfb54679 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.26.0-SNAPSHOT + 3.26.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index cb3ec69cae6..e83c178d656 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.26.0-SNAPSHOT + 3.26.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.26.0 diff --git a/pom.xml b/pom.xml index eb394089e8d..e128f2ddbef 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.26.0-SNAPSHOT + 3.26.0 extra/pom.xml From 1e66556c9c07b5b0dbae1d7dc0b8154662168b27 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Tue, 20 May 2025 22:40:44 -0400 Subject: [PATCH 05/51] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index ab2c814cdcf..2b519bf7492 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.26.0 + 3.27.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 24943ec7599..01fab8143a8 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index e7ec68fb357..d5a5f3f3203 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index d21f13f4e8b..8037f32a55f 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index baf74fb1b70..b951e1e2093 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index c179e532aee..8c92f5df82a 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 02db0f3a543..7117b2e7c14 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index d043e6fa2b7..39c22b2bca8 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0 + 3.27.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 75edfb54679..200e5f7458d 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.26.0 + 3.27.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index e83c178d656..140d04b95a9 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.26.0 + 3.27.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.26.0 + HEAD diff --git a/pom.xml b/pom.xml index e128f2ddbef..fc4794a1772 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.26.0 + 3.27.0-SNAPSHOT extra/pom.xml From 5aeaea2acb74f21c42707ca2cf4e913a4c1efe5a Mon Sep 17 00:00:00 2001 From: Armando Carballo <109094234+armando-fs@users.noreply.github.com> Date: Tue, 3 Jun 2025 06:58:42 -0600 Subject: [PATCH 06/51] New Adapter: 152 Media - Adkernel alias (#3829) --- .../resources/bidder-config/adkernel.yaml | 1 + .../prebid/server/it/OneFiveTwoMediaTest.java | 33 +++++++++++++ .../org/prebid/server/it/RxNetworkTest.java | 33 +++++++++++++ .../152media/test-152media-bid-request.json | 47 +++++++++++++++++++ .../152media/test-152media-bid-response.json | 23 +++++++++ .../test-auction-152media-request.json | 23 +++++++++ .../test-auction-152media-response.json | 44 +++++++++++++++++ .../test-auction-rxnetwork-request.json | 23 +++++++++ .../test-auction-rxnetwork-response.json | 44 +++++++++++++++++ .../rxnetwork/test-rxnetwork-bid-request.json | 47 +++++++++++++++++++ .../test-rxnetwork-bid-response.json | 23 +++++++++ 11 files changed, 341 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/OneFiveTwoMediaTest.java create mode 100644 src/test/java/org/prebid/server/it/RxNetworkTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-response.json diff --git a/src/main/resources/bidder-config/adkernel.yaml b/src/main/resources/bidder-config/adkernel.yaml index de3e735d982..2e521696c36 100644 --- a/src/main/resources/bidder-config/adkernel.yaml +++ b/src/main/resources/bidder-config/adkernel.yaml @@ -4,6 +4,7 @@ adapters: endpoint-compression: gzip aliases: rxnetwork: ~ + 152media: ~ meta-info: maintainer-email: prebid-dev@adkernel.com app-media-types: diff --git a/src/test/java/org/prebid/server/it/OneFiveTwoMediaTest.java b/src/test/java/org/prebid/server/it/OneFiveTwoMediaTest.java new file mode 100644 index 00000000000..0a09b348cef --- /dev/null +++ b/src/test/java/org/prebid/server/it/OneFiveTwoMediaTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class OneFiveTwoMediaTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFrom152Media() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernel-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/152media/test-152media-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/152media/test-152media-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/152media/test-auction-152media-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/152media/test-auction-152media-response.json", response, + singletonList("152media")); + } +} diff --git a/src/test/java/org/prebid/server/it/RxNetworkTest.java b/src/test/java/org/prebid/server/it/RxNetworkTest.java new file mode 100644 index 00000000000..139c70d3288 --- /dev/null +++ b/src/test/java/org/prebid/server/it/RxNetworkTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class RxNetworkTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromRxNetwork() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adkernel-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/rxnetwork/test-rxnetwork-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/rxnetwork/test-rxnetwork-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/rxnetwork/test-auction-rxnetwork-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/rxnetwork/test-auction-rxnetwork-response.json", response, + singletonList("rxnetwork")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-request.json new file mode 100644 index 00000000000..2c1d5b8f6bd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-request.json @@ -0,0 +1,47 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.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/152media/test-152media-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-response.json new file mode 100644 index 00000000000..03821c0471a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-152media-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 2.25, + "cid": "1001", + "crid": "2002", + "adid": "2002", + "adm": "", + "mtype": 1, + "adomain": [ + "tag-example.com" + ] + } + ] + } + ], + "bidid": "bid_id" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-request.json b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-request.json new file mode 100644 index 00000000000..2a61eb31bef --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "152media": { + "zoneId": 101 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-response.json b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-response.json new file mode 100644 index 00000000000..df798145a34 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/152media/test-auction-152media-response.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 2.25, + "adm": "", + "adid": "2002", + "adomain": [ + "tag-example.com" + ], + "cid": "1001", + "crid": "2002", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "152media" + } + }, + "origbidcpm": 2.25 + } + } + ], + "seat": "152media", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "152media": "{{ 152media.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-request.json new file mode 100644 index 00000000000..2a60e71d4e7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "rxnetwork": { + "zoneId": 101 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-response.json new file mode 100644 index 00000000000..5ddcd7b93d6 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-auction-rxnetwork-response.json @@ -0,0 +1,44 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 2.25, + "adm": "", + "adid": "2002", + "adomain": [ + "tag-example.com" + ], + "cid": "1001", + "crid": "2002", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "rxnetwork" + } + }, + "origbidcpm": 2.25 + } + } + ], + "seat": "rxnetwork", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "rxnetwork": "{{ rxnetwork.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-request.json new file mode 100644 index 00000000000..2c1d5b8f6bd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-request.json @@ -0,0 +1,47 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.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/rxnetwork/test-rxnetwork-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-response.json new file mode 100644 index 00000000000..03821c0471a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rxnetwork/test-rxnetwork-bid-response.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 2.25, + "cid": "1001", + "crid": "2002", + "adid": "2002", + "adm": "", + "mtype": 1, + "adomain": [ + "tag-example.com" + ] + } + ] + } + ], + "bidid": "bid_id" +} From 7037dba7a03c0567596053afc404799dedd54215 Mon Sep 17 00:00:00 2001 From: przemkaczmarek <167743744+przemkaczmarek@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:59:17 +0200 Subject: [PATCH 07/51] EPlanning: Add schain support (#3914) --- .../bidder/eplanning/EplanningBidder.java | 59 ++++++ .../bidder/eplanning/EplanningBidderTest.java | 175 ++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java index aab1f5b7e86..09c607d7451 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java @@ -8,11 +8,15 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; @@ -29,6 +33,7 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.eplanning.ExtImpEplanning; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -45,6 +50,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -269,6 +275,12 @@ private String resolveRequestUri(BidRequest request, List requestsString uriBuilder.addParameter("app", REQUEST_TARGET_INVENTORY); } + String schain = getSchainParameter(request.getSource()); + if (schain != null) { + schain = schain.replace(" ", "%20"); + uriBuilder.addParameter("sch", schain); + } + return uriBuilder.toString(); } @@ -280,6 +292,53 @@ private static URL parseUrl(String url) { } } + private String getSchainParameter(Source source) { + return Optional.ofNullable(source) + .map(Source::getExt) + .map(ExtSource::getSchain) + .map(this::resolveSupplyChain) + .orElse(null); + } + + private String resolveSupplyChain(SupplyChain schain) { + final List nodes = schain.getNodes(); + if (CollectionUtils.isEmpty(nodes) || nodes.size() > 2) { + return null; + } + + final StringBuilder schainBuilder = new StringBuilder(); + + schainBuilder.append(schain.getVer()); + schainBuilder.append(","); + schainBuilder.append(ObjectUtils.defaultIfNull(schain.getComplete(), 0)); + for (SupplyChainNode node : schain.getNodes()) { + schainBuilder.append("!"); + schainBuilder.append(StringUtils.defaultString(node.getAsi())); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getSid())); + schainBuilder.append(","); + + schainBuilder.append(node.getHp() != null ? node.getHp() : StringUtils.EMPTY); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getRid())); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getName())); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getDomain())); + schainBuilder.append(","); + + schainBuilder.append(node.getExt() == null + ? StringUtils.EMPTY + : mapper.encodeToString(node.getExt())); + } + + return schainBuilder.toString(); + } + /** * Converts response to {@link List} of {@link BidderBid}s with {@link List} of errors. * Handles cases when response status is different to OK 200. diff --git a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java index 00ba9278765..8b94f14c737 100644 --- a/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/eplanning/EplanningBidderTest.java @@ -8,6 +8,9 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.netty.handler.codec.http.HttpHeaderValues; @@ -25,6 +28,7 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.eplanning.ExtImpEplanning; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -35,6 +39,7 @@ import java.util.function.Function; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static org.assertj.core.api.Assertions.assertThat; @@ -413,6 +418,176 @@ public void makeHttpRequestsShouldSetCorrectUriWithApp() { + "appn=appName&appid=id&ifa=ifa&app=1"); } + @Test + public void makeHttpRequestsShouldNotAppendSchainIfSourceIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(null) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfExtIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(Source.builder().ext(null).build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfSchainIsNull() { + // given + final Source source = Source.builder() + .ext(ExtSource.of(null)) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfNoNodes() { + // given + final SupplyChain supplyChain = SupplyChain.of(1, emptyList(), "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldNotAppendSchainIfNodeCountIsGreaterThan2() { + // given + final List nodes = asList( + SupplyChainNode.of("asi1", "sid1", "rid1", "name1", "domain1", 1, null), + SupplyChainNode.of("asi2", "sid2", "rid2", "name2", "domain2", 1, null), + SupplyChainNode.of("asi3", "sid3", "rid3", "name3", "domain3", 1, null) + ); + final SupplyChain supplyChain = SupplyChain.of(1, nodes, "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri).doesNotContain("sch="); + } + + @Test + public void makeHttpRequestsShouldAppendSchainForUpToTwoNodes() { + // given + final List nodes = asList( + SupplyChainNode.of("asi1", "sid1", "rid1", "name1", "domain1", 1, null), + SupplyChainNode.of("asi2", "sid2", null, null, "domain2", 1, null) + ); + final SupplyChain supplyChain = SupplyChain.of(1, nodes, "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + final String uri = result.getValue().get(0).getUri(); + assertThat(uri) + .contains("sch=") + .contains("%21asi1%2Csid1%2C1%2Crid1%2Cname1%2Cdomain1%2C") + .contains("%21asi2%2Csid2%2C1%2C%2C%2Cdomain2%2C"); + } + + @Test + public void makeHttpRequestsShouldUrlEncodeSchainFieldsCorrectly() { + // given + final List nodes = singletonList( + SupplyChainNode.of( + "a si", + "s/id", + null, + "r:id", + "na me", + 1, + jacksonMapper.mapper().createObjectNode().put("k", "v val")) + ); + final SupplyChain supplyChain = SupplyChain.of(0, nodes, "1.0", null); + final Source source = Source.builder() + .ext(ExtSource.of(supplyChain)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(givenImp(identity()))) + .source(source) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String uri = result.getValue().get(0).getUri(); + + assertThat(uri).contains("&sch="); + assertThat(uri).contains("1.0%2C0"); + assertThat(uri).contains("%21a%2520si"); + assertThat(uri).contains("s%2Fid"); + assertThat(uri).contains("r%3Aid"); + assertThat(uri).contains("na%2520me"); + assertThat(uri).contains("%7B%22k%22%3A%22v%2520val%22%7D"); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given From 7a3fc81f71ab6f67e771cc215c016565f0245c71 Mon Sep 17 00:00:00 2001 From: kim-ng93 <76963037+kim-ng93@users.noreply.github.com> Date: Tue, 3 Jun 2025 05:59:38 -0700 Subject: [PATCH 08/51] Port Kueez: New Adapter (#3930) --- .../bidder/kueezrtb/KueezRtbBidder.java | 119 +++++++++ .../ext/request/kueezrtb/KueezRtbImpExt.java | 12 + .../config/bidder/KueezRtbConfiguration.java | 41 ++++ .../resources/bidder-config/kueezrtb.yaml | 20 ++ .../static/bidder-params/kueezrtb.json | 18 ++ .../biddersparams/BidderParams.groovy | 1 + .../bidder/kueezrtb/KueezRtbBidderTest.java | 227 ++++++++++++++++++ .../org/prebid/server/it/KueezRtbTest.java | 33 +++ .../test-auction-kueezrtb-request.json | 24 ++ .../test-auction-kueezrtb-response.json | 43 ++++ .../kueezrtb/test-kueezrtb-bid-request.json | 56 +++++ .../kueezrtb/test-kueezrtb-bid-response.json | 24 ++ .../server/it/test-application.properties | 2 + 13 files changed, 620 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/KueezRtbConfiguration.java create mode 100644 src/main/resources/bidder-config/kueezrtb.yaml create mode 100644 src/main/resources/static/bidder-params/kueezrtb.json create mode 100644 src/test/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/KueezRtbTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidder.java b/src/main/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidder.java new file mode 100644 index 00000000000..2cc1e3bfe5a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidder.java @@ -0,0 +1,119 @@ +package org.prebid.server.bidder.kueezrtb; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.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.kueezrtb.KueezRtbImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class KueezRtbBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public KueezRtbBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final KueezRtbImpExt impExt = parseImpExt(imp); + requests.add(makeHttpRequest(bidRequest, imp, impExt)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return Result.of(requests, errors); + } + + private KueezRtbImpExt parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private HttpRequest makeHttpRequest(BidRequest bidRequest, Imp imp, KueezRtbImpExt impExt) { + final BidRequest modifiedBidRequest = bidRequest.toBuilder().imp(Collections.singletonList(imp)).build(); + final String uri = endpointUrl + HttpUtil.encodeUrl(StringUtils.defaultString(impExt.getConnectionId()).trim()); + + return BidderUtil.defaultRequest(modifiedBidRequest, uri, mapper); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + final BidType mediaType = getMediaTypeForBid(bid); + return BidderBid.of(bid, mediaType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getMediaTypeForBid(Bid bid) { + final Integer mType = bid.getMtype(); + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException("Could not define bid type for imp: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java new file mode 100644 index 00000000000..5abaf1be0f5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.kueezrtb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class KueezRtbImpExt { + + @JsonProperty("cId") + String connectionId; + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/KueezRtbConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KueezRtbConfiguration.java new file mode 100644 index 00000000000..06e89467d0c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/KueezRtbConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.kueezrtb.KueezRtbBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/kueezrtb.yaml", factory = YamlPropertySourceFactory.class) +public class KueezRtbConfiguration { + + private static final String BIDDER_NAME = "kueezrtb"; + + @Bean("kueezrtbConfigurationProperties") + @ConfigurationProperties("adapters.kueezrtb") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps kueezrtbBidderDeps(BidderConfigurationProperties kueezrtbConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(kueezrtbConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new KueezRtbBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/kueezrtb.yaml b/src/main/resources/bidder-config/kueezrtb.yaml new file mode 100644 index 00000000000..b26c7ad4ea8 --- /dev/null +++ b/src/main/resources/bidder-config/kueezrtb.yaml @@ -0,0 +1,20 @@ +adapters: + kueezrtb: + endpoint: https://prebidsrvr.kueezrtb.com/openrtb/ + endpoint-compression: gzip + meta-info: + maintainer-email: rtb@kueez.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 1165 + usersync: + cookie-family-name: kueezrtb + iframe: + url: https://sync.kueezrtb.com/api/user/html/62ce79e7dd15099534ae5e04?pbs=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}} + support-cors: false + uid-macro: '${userId}' diff --git a/src/main/resources/static/bidder-params/kueezrtb.json b/src/main/resources/static/bidder-params/kueezrtb.json new file mode 100644 index 00000000000..29685df6f00 --- /dev/null +++ b/src/main/resources/static/bidder-params/kueezrtb.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Kueez RTB Adapter Params", + "description": "A schema which validates params accepted by the Kueez RTB adapter", + "type": "object", + "properties": { + "cId": { + "type": "string", + "description": "The connection id.", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_]+$" + } + }, + "required": [ + "cId" + ], + "additionalProperties": false +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy b/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy index 140f25c410f..3b6df42c7de 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy @@ -17,4 +17,5 @@ class BidderParams { def appid def placementid def dependencies + def additionalProperties } diff --git a/src/test/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidderTest.java b/src/test/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidderTest.java new file mode 100644 index 00000000000..d98e0d65958 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidderTest.java @@ -0,0 +1,227 @@ +package org.prebid.server.bidder.kueezrtb; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.kueezrtb.KueezRtbImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; + +class KueezRtbBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.host.com/prebid/bid/"; + + private final KueezRtbBidder target = new KueezRtbBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpoint() { + assertThatIllegalArgumentException().isThrownBy(() -> new KueezRtbBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldMakeOneRequestPerImp() { + // given + final Imp imp1 = givenImp(imp -> imp.id("imp1")); + final Imp imp2 = givenImp(imp -> imp.id("imp2")); + final BidRequest request = BidRequest.builder().imp(List.of(imp1, imp2)).build(); + + // when + final Result>> result = target.makeHttpRequests(request); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactlyInAnyOrder(imp1, imp2); + } + + @Test + public void makeHttpRequestsShouldContainExpectedHeaders() { + // given + final BidRequest request = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(request); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)); + } + + @Test + public void makeHttpRequestsShouldUseCorrectUri() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.host.com/prebid/bid/cid"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorOnInvalidImp() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> + imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().getFirst().getMessage()).startsWith("Cannot deserialize value"); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyInvalid() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWithTypeSuccessfully() throws JsonProcessingException { + // given + final Bid bannerBid = Bid.builder().impid("imp1").mtype(1).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsOnly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidWithTypeSuccessfully() throws JsonProcessingException { + // given + final Bid videoBid = Bid.builder().impid("imp2").mtype(2).build(); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsOnly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenImpTypeIsNotSupported() throws JsonProcessingException { + // given + final Bid audioBid = Bid.builder().id("bidId3").impid("id3").mtype(3).build(); + + final BidderCall httpCall = givenHttpCall(givenBidResponse(audioBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + // then + assertThat(result.getErrors()).containsOnly(badServerResponse("Could not define bid type for imp: id3")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenMTypeIsNotIncluded() throws JsonProcessingException { + // given + final Bid bannerBid = Bid.builder().id("bidId1").impid("id1").mtype(1).build(); + final Bid bidWithoutMtype = Bid.builder().id("bidId2").impid("id2").mtype(null).build(); + + final BidderCall httpCall = givenHttpCall( + givenBidResponse(bannerBid, bidWithoutMtype)); + + // when + final Result> result = target.makeBids(httpCall, null); + // then + assertThat(result.getErrors()).containsOnly(badServerResponse("Could not define bid type for imp: id2")); + assertThat(result.getValue()).containsOnly(BidderBid.of(bannerBid, banner, "USD")); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder().imp(singletonList(givenImp(impCustomizer)))).build(); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(mapper.valueToTree(ExtPrebid.of(null, KueezRtbImpExt.of("cid"))))) + .build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } +} diff --git a/src/test/java/org/prebid/server/it/KueezRtbTest.java b/src/test/java/org/prebid/server/it/KueezRtbTest.java new file mode 100644 index 00000000000..94b1d597ca2 --- /dev/null +++ b/src/test/java/org/prebid/server/it/KueezRtbTest.java @@ -0,0 +1,33 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class KueezRtbTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromKueez() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/kueezrtb-exchange/test_cid_123")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/kueezrtb/test-kueezrtb-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/kueezrtb/test-kueezrtb-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/kueezrtb/test-auction-kueezrtb-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/kueezrtb/test-auction-kueezrtb-response.json", response, List.of("kueezrtb")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-request.json new file mode 100644 index 00000000000..846277abcde --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-request.json @@ -0,0 +1,24 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "kueezrtb": { + "cId": "test_cid_123" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-response.json new file mode 100644 index 00000000000..c1c319df7c3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-auction-kueezrtb-response.json @@ -0,0 +1,43 @@ +{ + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "some-bid-id", + "impid": "some-impression-id", + "exp": 300, + "price": 1, + "adm": "
Some creative
", + "adid": "some-ad-id", + "cid": "test", + "crid": "some-creative-id", + "w": 320, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "kueezrtb" + } + }, + "origbidcpm": 1 + } + } + ], + "seat": "kueezrtb", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "kueezrtb": "{{ kueezrtb.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-request.json new file mode 100644 index 00000000000..67311c43097 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 320, + "h": 250 + }, + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "cId": "test_cid_123" + } + } + } + ], + "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" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "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/kueezrtb/test-kueezrtb-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-response.json new file mode 100644 index 00000000000..36bff877013 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/kueezrtb/test-kueezrtb-bid-response.json @@ -0,0 +1,24 @@ +{ + "id": "some-request-id", + "cur": "", + "bidid": "some-bid-id", + "seatbid": [ + { + "bid": [ + { + "adm": "
Some creative
", + "id": "some-bid-id", + "impid": "some-impression-id", + "h": 250, + "w": 320, + "price": 1, + "adid": "some-ad-id", + "cid": "test", + "crid": "some-creative-id", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 450ae419cb5..44159e6e071 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -281,6 +281,8 @@ adapters.kobler.enabled=true adapters.kobler.endpoint=http://localhost:8090/kobler-exchange adapters.krushmedia.enabled=true adapters.krushmedia.endpoint=http://localhost:8090/krushmedia-exchange +adapters.kueezrtb.enabled=true +adapters.kueezrtb.endpoint=http://localhost:8090/kueezrtb-exchange/ adapters.lemmadigital.enabled=true adapters.lemmadigital.endpoint=http://localhost:8090/lemmadigital-exchange/{{PublisherID}}/{{AdUnit}} adapters.vungle.enabled=true From 323bc051915be2e1d09f56c4e18ebeeefd85f8f4 Mon Sep 17 00:00:00 2001 From: katherynhrabik <75269700+katherynhrabik@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:00:27 -0400 Subject: [PATCH 09/51] =?UTF-8?q?=D0=A1riteo:=20ortb=202.6=20support=20(#3?= =?UTF-8?q?969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/bidder-config/criteo.yaml | 4 ++++ .../server/it/openrtb2/criteo/test-criteo-bid-request.json | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/resources/bidder-config/criteo.yaml b/src/main/resources/bidder-config/criteo.yaml index a863ba2beb8..53cefc5c240 100644 --- a/src/main/resources/bidder-config/criteo.yaml +++ b/src/main/resources/bidder-config/criteo.yaml @@ -1,6 +1,9 @@ adapters: criteo: + ortb-version: "2.6" endpoint: https://ssp-bidder.criteo.com/openrtb/pbs/auction/request?profile=230 + ortb: + multiformat-supported: true meta-info: maintainer-email: prebid@criteo.com app-media-types: @@ -13,6 +16,7 @@ adapters: - native supported-vendors: vendor-id: 91 + usersync: cookie-family-name: criteo redirect: diff --git a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-criteo-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-criteo-bid-request.json index 3260e9eebe5..0ad1b0608e8 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-criteo-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-criteo-bid-request.json @@ -41,9 +41,7 @@ "ip": "193.168.244.1" }, "regs": { - "ext": { "gdpr": 0 - } }, "ext": { "prebid": { From 6f3c529e87214ae1f1614a369f5d91efddfc9c59 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:00:39 +0200 Subject: [PATCH 10/51] Rubicon Adapter: set additional meta fields (#3960) Co-authored-by: osulzhenko --- .../server/bidder/rubicon/RubiconBidder.java | 31 +++++++++- .../bidder/rubicon/RubiconBidderTest.java | 56 +++++++++++++++++++ .../test-auction-magnite-response.json | 3 + .../magnite/test-magnite-bid-response.json | 4 +- .../test-auction-rubicon-response.json | 3 + .../rubicon/test-rubicon-bid-response.json | 4 +- 6 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index ad63bb5fe0f..d66fd90adf5 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -1595,8 +1595,10 @@ private ObjectNode prepareBidExt(RubiconBid bid, final Integer networkId = resolveNetworkId(seatBid); final String seat = seatBid.getSeat(); final String rendererUrl = resolveRendererUrl(imp, meta, bidType, hasApexRenderer); + final List advertiserDomains = bid.getAdomain(); + final Integer advertiserId = resolveAdvertiserId(bidExt); - if (ObjectUtils.allNull(networkId, rendererUrl, seat)) { + if (ObjectUtils.allNull(networkId, rendererUrl, seat, advertiserDomains, advertiserId)) { return bidExt; } @@ -1606,6 +1608,8 @@ private ObjectNode prepareBidExt(RubiconBid bid, .networkId(networkId) .seat(seat) .rendererUrl(rendererUrl) + .advertiserId(advertiserId) + .advertiserDomains(advertiserDomains) .build(); final ExtBidPrebid modifiedExtBidPrebid = extBidPrebid != null @@ -1652,6 +1656,31 @@ private static Boolean isVideoMetaMediaType(ExtBidPrebidMeta meta) { .orElse(false); } + private static Integer resolveAdvertiserId(ObjectNode bidExt) { + return Optional.ofNullable(bidExt) + .map(ext -> ext.get("rp")) + .filter(JsonNode::isObject) + .map(rp -> rp.get("advid")) + .map(RubiconBidder::convertToInt) + .orElse(null); + } + + private static Integer convertToInt(JsonNode jsonNode) { + if (jsonNode.canConvertToInt()) { + return jsonNode.asInt(); + } + + if (jsonNode.isTextual()) { + try { + return Integer.parseInt(jsonNode.asText()); + } catch (NumberFormatException e) { + return null; + } + } + + return null; + } + private String resolveBidId(Imp rubiconImp, RubiconBid bid) { return generateBidId ? Optional.ofNullable(rubiconImp) diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index 5ed82129c2f..49cb2a20e97 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -3430,6 +3430,62 @@ public void makeBidsShouldSetSeatToMetaSeat() throws JsonProcessingException { .containsExactly(expectedBidExt); } + @Test + public void makeBidsShouldSetBidExtRpAdvidToMetaAdvertiserId() throws JsonProcessingException { + // given + final ObjectNode givenBidExt = mapper.createObjectNode() + .set("rp", mapper.createObjectNode().put("advid", "1")); + + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + mapper.writeValueAsString(RubiconBidResponse.builder() + .cur("USD") + .seatbid(singletonList(RubiconSeatBid.builder() + .bid(singletonList(givenRubiconBid(bid -> bid.ext(givenBidExt)))) + .build())) + .build())); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + final ObjectNode expectedBidExt = givenBidExt.deepCopy() + .set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode().put("advertiserId", 1))); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly(expectedBidExt); + } + + @Test + public void makeBidsShouldSetBidAdomainToMetaAdvertiserDomains() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidRequest(identity()), + mapper.writeValueAsString(RubiconBidResponse.builder() + .cur("USD") + .seatbid(singletonList(RubiconSeatBid.builder() + .bid(singletonList(givenRubiconBid(bid -> bid.adomain(List.of("A", "B"))))) + .build())) + .build())); + + // when + final Result> result = target.makeBids(httpCall, givenBidRequest(identity())); + + // then + final ObjectNode expectedBidExt = mapper.valueToTree( + ExtPrebid.of(ExtBidPrebid.builder() + .meta(ExtBidPrebidMeta.builder().advertiserDomains(List.of("A", "B")).build()) + .build(), null)); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly(expectedBidExt); + } + @Test public void makeBidsShouldSetSeatBuyerToMetaNetworkId() throws JsonProcessingException { // given diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json index 0f321a8402e..b0a256793f6 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json @@ -15,6 +15,9 @@ "w": 300, "h": 250, "ext": { + "rp": { + "advid": "1" + }, "prebid": { "meta": { "advertiserId": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-response.json index dd045a80c89..bd9eef22d1f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-response.json @@ -15,9 +15,11 @@ "h": 250, "w": 300, "ext": { + "rp": { + "advid": "1" + }, "prebid": { "meta": { - "advertiserId": 1, "secondaryCatIds": [ "id1", "id2" diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json index 3baa5fddcda..f7136a7cffc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json @@ -15,6 +15,9 @@ "w": 300, "h": 250, "ext": { + "rp": { + "advid": "1" + }, "prebid": { "meta": { "advertiserId": 1, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-response.json index dd045a80c89..bd9eef22d1f 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-response.json @@ -15,9 +15,11 @@ "h": 250, "w": 300, "ext": { + "rp": { + "advid": "1" + }, "prebid": { "meta": { - "advertiserId": 1, "secondaryCatIds": [ "id1", "id2" From fba26a63f471267f117de83017a4970add03dea1 Mon Sep 17 00:00:00 2001 From: Jim Thario <33331284+JimTharioAmazon@users.noreply.github.com> Date: Tue, 3 Jun 2025 06:00:53 -0700 Subject: [PATCH 11/51] Bump spring.boot.version to 3.4.5 (#3980) --- extra/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/pom.xml b/extra/pom.xml index 140d04b95a9..352acbd59f8 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -33,7 +33,7 @@ 10.17.0 - 3.4.4 + 3.4.5 4.5.14 2.0.1.Final 4.4 From a87768751e72c8140d7c12691f7817d558041d5e Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:05:19 +0200 Subject: [PATCH 12/51] Add Account auction.cache.enabled Flag (#3955) --- docs/application-settings.md | 1 + .../server/auction/ExchangeService.java | 15 +- .../settings/model/AccountAuctionConfig.java | 2 + .../settings/model/AccountCacheConfig.java | 9 ++ .../model/config/AccountAuctionConfig.groovy | 1 + .../model/config/AccountCacheConfig.groovy | 9 ++ .../server/functional/tests/CacheSpec.groovy | 135 ++++++++++++++++++ .../ResponseCorrectionSpec.groovy | 57 ++++++++ .../server/auction/ExchangeServiceTest.java | 42 ++++++ 9 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountCacheConfig.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AccountCacheConfig.groovy diff --git a/docs/application-settings.md b/docs/application-settings.md index d99607a1477..9fa2bff4917 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -50,6 +50,7 @@ Keep in mind following restrictions: - `auction.preferredmediatype..` - that will be left for that doesn't support multi-format. Other media types will be removed. Acceptable values: `banner`, `video`, `audio`, `native`. - `auction.privacysandbox.cookiedeprecation.enabled` - boolean that turns on setting and reading of the Chrome Privacy Sandbox testing label header. Defaults to false. - `auction.privacysandbox.cookiedeprecation.ttlsec` - if the above setting is true, how long to set the receive-cookie-deprecation cookie's expiration +- `auction.cache.enabled` - enables bids caching for account if true. Defaults to true. - `privacy.gdpr.enabled` - enables gdpr verifications if true. Has higher priority than configuration in application.yaml. - `privacy.gdpr.eea-countries` - overrides the host-level list of 2-letter country codes where TCF processing is applied diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 9a1326b4837..1db72fcc649 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -94,6 +94,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountCacheConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ListUtil; import org.prebid.server.util.PbsUtil; @@ -241,7 +243,7 @@ private Future runAuction(AuctionContext receivedContext) { final List storedAuctionResponses = new ArrayList<>(); final BidderAliases aliases = aliases(bidRequest, account); - final BidRequestCacheInfo cacheInfo = bidRequestCacheInfo(bidRequest); + final BidRequestCacheInfo cacheInfo = bidRequestCacheInfo(bidRequest, account); final Map bidderToMultiBid = bidderToMultiBids(bidRequest, debugWarnings); receivedContext.getBidRejectionTrackers().putAll(makeBidRejectionTrackers(bidRequest, aliases)); @@ -311,12 +313,18 @@ private static ExtRequestTargeting targeting(BidRequest bidRequest) { return prebid != null ? prebid.getTargeting() : null; } - private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { + private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest, Account account) { + final boolean cachingEnabled = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getCache) + .map(AccountCacheConfig::getEnabled) + .orElse(true); + final ExtRequestTargeting targeting = targeting(bidRequest); final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; - if (targeting != null && cache != null) { + if (cachingEnabled && targeting != null && cache != null) { final boolean shouldCacheBids = cache.getBids() != null; final boolean shouldCacheVideoBids = cache.getVastxml() != null; final boolean shouldCacheWinningBidsOnly = !targeting.getIncludebidderkeys() @@ -345,6 +353,7 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { .build(); } } + return BidRequestCacheInfo.noCache(); } diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index ac7da04dd31..96e12b0e927 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -53,4 +53,6 @@ public class AccountAuctionConfig { @JsonProperty("paaformat") PaaFormat paaFormat; + + AccountCacheConfig cache; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountCacheConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCacheConfig.java new file mode 100644 index 00000000000..72f205865b8 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountCacheConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountCacheConfig { + + Boolean enabled; +} 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 8dc9831e3fa..984ef6409b2 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 @@ -22,6 +22,7 @@ class AccountAuctionConfig { Boolean debugAllow AccountBidValidationConfig bidValidations AccountEventsConfig events + AccountCacheConfig cache AccountPriceFloorsConfig priceFloors Targeting targeting PaaFormat paaformat diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountCacheConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountCacheConfig.groovy new file mode 100644 index 00000000000..121e32f03cd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountCacheConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountCacheConfig { + + Boolean enabled +} 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 e145e5a972f..2aec80a7e0a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountCacheConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountEventsConfig import org.prebid.server.functional.model.db.Account @@ -14,6 +15,8 @@ import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils +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.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO @@ -42,6 +45,9 @@ class CacheSpec extends BaseSpec { def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) def request = VtrackRequest.getDefaultVtrackRequest(creative) + and: "Flush metrics" + flushMetrics(defaultPbsService) + when: "PBS processes vtrack request" defaultPbsService.sendVtrackRequest(request, accountId) @@ -468,6 +474,9 @@ class CacheSpec extends BaseSpec { "<${impression}> <![CDATA[ ]]> " def request = VtrackRequest.getDefaultVtrackRequest(creative) + and: "Flush metrics" + flushMetrics(defaultPbsService) + when: "PBS processes vtrack request" defaultPbsService.sendVtrackRequest(request, accountId) @@ -492,4 +501,130 @@ class CacheSpec extends BaseSpec { PBSUtils.getRandomCase(" inline ") | " ${PBSUtils.getRandomCase(" impression ")} $PBSUtils.randomNumber " " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " } + + def "PBS should cache bids and add targeting values when account cache config #accountAuctionConfig"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, CACHE_REQUEST_OK_GLOBAL_METRIC) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.enableCache() + } + + 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) + + and: "Default bid response" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS response targeting contains bidder specific keys" + def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting + assert targetingKeyMap.containsKey('hb_cache_id') + assert targetingKeyMap.containsKey("hb_cache_id_${GENERIC}".toString()) + assert targetingKeyMap.containsKey('hb_uuid') + assert targetingKeyMap.containsKey("hb_uuid_${GENERIC}".toString()) + + and: "Metrics should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[CACHE_REQUEST_OK_GLOBAL_METRIC] == initialValue + 1 + assert metrics[CACHE_REQUEST_OK_ACCOUNT_METRIC.formatted(bidRequest.accountId)] == 1 + + where: + accountAuctionConfig << [ + new AccountAuctionConfig(), + new AccountAuctionConfig(cache: new AccountCacheConfig()), + new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: null)), + new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: true)) + ] + } + + def "PBS shouldn't cache bids and add targeting values when account cache config disabled"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, CACHE_REQUEST_OK_GLOBAL_METRIC) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.enableCache() + } + + and: "Account with cache config" + def accountAuctionConfig = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: false)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Default bid response" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call PBC" + assert !prebidCache.getRequestCount(bidRequest.imp[0].id) + + and: "PBS response targeting shouldn't contains bidder specific keys" + def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting + assert !targetingKeyMap.containsKey('hb_cache_id') + assert !targetingKeyMap.containsKey("hb_cache_id_${GENERIC}".toString()) + assert !targetingKeyMap.containsKey('hb_uuid') + assert !targetingKeyMap.containsKey("hb_uuid_${GENERIC}".toString()) + + and: "Metrics shouldn't be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[CACHE_REQUEST_OK_GLOBAL_METRIC] == initialValue + assert !metrics[CACHE_REQUEST_OK_ACCOUNT_METRIC.formatted(bidRequest.accountId)] + } + + def "PBS should update prebid_cache.creative_size.xml metric when account cache config #enabledCacheConcfig"() { + given: "Current value of metric prebid_cache.requests.ok" + def okInitialValue = getCurrentMetricValue(defaultPbsService, CACHE_REQUEST_OK_GLOBAL_METRIC) + + and: "Default VtrackRequest" + def accountId = PBSUtils.randomNumber.toString() + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Create and save enabled events config in account" + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.auction = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: enabledCacheConcfig)) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendVtrackRequest(request, accountId) + + then: "prebid_cache.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def creativeSize = creative.bytes.length + assert metrics[CACHE_REQUEST_OK_GLOBAL_METRIC] == okInitialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics[CACHE_REQUEST_OK_ACCOUNT_METRIC.formatted(accountId)] == 1 + assert metrics[XML_CREATIVE_SIZE_ACCOUNT_METRIC.formatted(accountId)] == creativeSize + + where: + enabledCacheConcfig << [null, false, true] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy index b4cbdd3fb88..2426a3fd316 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy @@ -1,5 +1,7 @@ package org.prebid.server.functional.tests.module.responsecorrenction +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountCacheConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration import org.prebid.server.functional.model.config.AppVideoHtml @@ -576,6 +578,61 @@ class ResponseCorrectionSpec extends ModuleBaseSpec { assert !response.ext.warnings } + def "PBS should modify response when requested video impression respond with invalid adm VAST keyword and disabled cache config"() { + given: "Start up time" + def start = Instant.now() + + and: "Default bid request with APP and Video imp" + def bidRequest = getDefaultVideoRequest(APP) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].setAdm(PBSUtils.getRandomCase(admValue)) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account with enabled response correction module" + def accountWithResponseCorrectionModule = accountConfigWithResponseCorrectionModule(bidRequest).tap { + config.auction = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: false)) + } + accountDao.save(accountWithResponseCorrectionModule) + + when: "PBS processes auction request" + def response = pbsServiceWithResponseCorrectionModule.sendAuctionRequest(bidRequest) + + then: "PBS should emit log" + def logsByTime = pbsServiceWithResponseCorrectionModule.getLogsByTime(start) + def bidId = bidResponse.seatbid[0].bid[0].id + def responseCorrection = getLogsByText(logsByTime, bidId) + assert responseCorrection.size() == 1 + assert responseCorrection.any { + it.contains("Bid $bidId of bidder generic: changing media type to banner" as String) + } + + and: "Response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "Response should contain single seatBid with proper media type" + assert response.seatbid.bid.ext.prebid.type.flatten() == [BANNER] + + and: "Response should contain single seatBid with proper meta media type" + assert response.seatbid.bid.ext.prebid.meta.mediaType.flatten() == [VIDEO.value] + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + where: + admValue << [ + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST${PBSUtils.randomString}", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST", + "<${' ' * PBSUtils.getRandomNumber(0, OPTIMAL_MAX_LENGTH)}VAST>", + "<${PBSUtils.randomString}VAST${' ' * PBSUtils.getRandomNumber(1, OPTIMAL_MAX_LENGTH)}" + ] + } + private static Account accountConfigWithResponseCorrectionModule(BidRequest bidRequest, Boolean enabledResponseCorrection = true, Boolean enabledAppVideoHtml = true) { def modulesConfig = new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection( enabled: enabledResponseCorrection, appVideoHtml: new AppVideoHtml(enabled: enabledAppVideoHtml))) diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index c13ca6eb56f..584825fa273 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -151,6 +151,7 @@ import org.prebid.server.settings.model.AccountAlternateBidderCodesBidder; import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountCacheConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.Ortb; @@ -1635,6 +1636,47 @@ public void shouldCallBidResponseCreatorWithWinningOnlyTrueWhenIncludeBidderKeys .containsOnly(true); } + @Test + public void shouldCallBidResponseCreatorWithCachingDisabledWhenCachingIsNotEnabledOnAccountLevel() { + // given + givenBidder("bidder1", mock(Bidder.class), givenEmptySeatBid()); + + final Bid thirdBid = Bid.builder().id("bidId3").impid("impId3").price(BigDecimal.valueOf(7.89)).build(); + givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(givenBidderBid(thirdBid)))); + + final ExtRequestTargeting targeting = givenTargeting(false); + + final BidRequest bidRequest = givenBidRequest(asList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(Map.of("bidder1", 1, "bidder2", 2), builder -> builder.id("impId2"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(targeting) + .cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(53, true), + ExtRequestPrebidCacheVastxml.of(34, true), true)) + .auctiontimestamp(1000L) + .build()))); + + // when + target.holdAuction(givenRequestContext( + bidRequest, + Account.builder() + .id("accountId") + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(true)) + .cache(AccountCacheConfig.of(false)) + .build()) + .build())); + + // then + final ArgumentCaptor auctionContextArgumentCaptor = + ArgumentCaptor.forClass(AuctionContext.class); + verify(bidResponseCreator).create( + auctionContextArgumentCaptor.capture(), + eq(BidRequestCacheInfo.noCache()), + eq(emptyMap())); + } + @Test public void shouldCallBidResponseCreatorWithWinningOnlyFalseWhenWinningOnlyIsNull() { // given From 40ea36712fd711a7028be1ab85a099f073808b28 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:05:30 +0200 Subject: [PATCH 13/51] Support bid rounding options (#3957) --- docs/application-settings.md | 5 + .../server/auction/BidResponseCreator.java | 3 +- .../org/prebid/server/auction/CpmRange.java | 35 +++- .../auction/TargetingKeywordsCreator.java | 15 +- .../BasicCategoryMappingService.java | 33 +++- .../CategoryMappingService.java | 2 + .../NoOpCategoryMappingService.java | 2 + .../model/AccountAuctionBidRoundingMode.java | 20 ++ .../settings/model/AccountAuctionConfig.java | 3 + .../model/config/AccountAuctionConfig.groovy | 4 + .../model/request/auction/BidRounding.groovy | 24 +++ .../server/functional/tests/AmpSpec.groovy | 2 +- .../server/functional/tests/BaseSpec.groovy | 20 +- .../functional/tests/BidRoundingSpec.groovy | 111 +++++++++++ .../PriceFloorsEnforcementSpec.groovy | 2 +- .../BasicCategoryMappingServiceTest.java | 83 ++++---- .../auction/BidResponseCreatorTest.java | 8 +- .../prebid/server/auction/CpmRangeTest.java | 177 +++++++++++++++--- .../auction/TargetingKeywordsCreatorTest.java | 38 ++-- 19 files changed, 478 insertions(+), 109 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy diff --git a/docs/application-settings.md b/docs/application-settings.md index 9fa2bff4917..7a4a72f38d1 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -25,6 +25,11 @@ There are two ways to configure application settings: database and file. This do - `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment - `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment - `auction.events.enabled` - enables events for account if true +- `auction.bid-rounding` - bid rounding options are: + - **down** - rounding down to the lower price bucket + - **up** - rounding up to the higher price bucket + - **timesplit** - 50% of the time rounding down to the lower PB and 50% of the time rounding up to the higher price bucket + - **true** - if the price >= 50% of the range, rounding up to the higher price bucket, otherwise rounding down - `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true. - `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false. - `auction.price-floors.fetch.url` - url to fetch price floors data from. diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 73dcc6b404a..ee28ddbe9d7 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -594,6 +594,7 @@ private Future createCategoryMapping(AuctionContext aucti return categoryMappingService.createCategoryMapping( bidderResponses, auctionContext.getBidRequest(), + auctionContext.getAccount(), auctionContext.getTimeoutContext().getTimeout()) .map(categoryMappingResult -> addCategoryMappingErrors(categoryMappingResult, auctionContext)); @@ -1561,7 +1562,7 @@ private Bid toBid(BidInfo bidInfo, final String categoryDuration = bidInfo.getCategory(); targetingKeywords = keywordsCreator != null ? keywordsCreator.makeFor( - bid, seat, isWinningBid, cacheId, bidType.getName(), videoCacheId, categoryDuration) + bid, seat, isWinningBid, cacheId, bidType.getName(), videoCacheId, categoryDuration, account) : null; } else { targetingKeywords = null; diff --git a/src/main/java/org/prebid/server/auction/CpmRange.java b/src/main/java/org/prebid/server/auction/CpmRange.java index 11ad6652438..00133373474 100644 --- a/src/main/java/org/prebid/server/auction/CpmRange.java +++ b/src/main/java/org/prebid/server/auction/CpmRange.java @@ -3,11 +3,16 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountAuctionBidRoundingMode; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; /** * Class for price operating with rules defined in {@link PriceGranularity} @@ -23,8 +28,8 @@ private CpmRange() { /** * Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in string format */ - public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity) { - final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity); + public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity, Account account) { + final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity, account); return value != null ? format(value, priceGranularity.getPrecision()) : StringUtils.EMPTY; } @@ -47,7 +52,7 @@ private static NumberFormat numberFormat(int precision) { * Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in {@link BigDecimal} * format */ - public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity) { + public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity, Account account) { if (cpm.compareTo(BigDecimal.ZERO) <= 0) { return null; } @@ -69,14 +74,32 @@ public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceG min = max; } - return increment != null ? calculate(cpm, min, increment) : null; + return increment != null ? calculate(cpm, min, increment, resolveRoundingMode(account)) : null; } - private static BigDecimal calculate(BigDecimal cpm, BigDecimal min, BigDecimal increment) { + private static BigDecimal calculate(BigDecimal cpm, + BigDecimal min, + BigDecimal increment, + RoundingMode roundingMode) { + return cpm .subtract(min) - .divide(increment, 0, RoundingMode.FLOOR) + .divide(increment, 0, roundingMode) .multiply(increment) .add(min); } + + private static RoundingMode resolveRoundingMode(Account account) { + final AccountAuctionBidRoundingMode accountRoundingMode = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidRounding) + .orElse(AccountAuctionBidRoundingMode.DOWN); + + return switch (accountRoundingMode) { + case DOWN -> RoundingMode.FLOOR; + case UP -> RoundingMode.CEILING; + case TRUE -> RoundingMode.HALF_UP; + case TIMESPLIT -> ThreadLocalRandom.current().nextBoolean() ? RoundingMode.FLOOR : RoundingMode.CEILING; + }; + } } diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 4fb92910ebf..b873210c1f3 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -3,6 +3,7 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.util.ArrayList; @@ -152,7 +153,8 @@ Map makeFor(Bid bid, String cacheId, String format, String vastCacheId, - String categoryDuration) { + String categoryDuration, + Account account) { final Map keywords = makeFor( bidder, @@ -164,7 +166,8 @@ Map makeFor(Bid bid, vastCacheId, categoryDuration, format, - bid.getDealid()); + bid.getDealid(), + account); if (resolver == null) { return truncateKeys(keywords); @@ -188,7 +191,8 @@ private Map makeFor(String bidder, String vastCacheId, String categoryDuration, String format, - String dealId) { + String dealId, + Account account) { final boolean includeDealBid = alwaysIncludeDeals && StringUtils.isNotEmpty(dealId); final KeywordMap keywordMap = new KeywordMap( @@ -198,7 +202,10 @@ private Map makeFor(String bidder, includeBidderKeys || includeDealBid, Collections.emptySet()); - final String roundedCpm = isPriceGranularityValid() ? CpmRange.fromCpm(price, priceGranularity) : DEFAULT_CPM; + final String roundedCpm = isPriceGranularityValid() + ? CpmRange.fromCpm(price, priceGranularity, account) + : DEFAULT_CPM; + keywordMap.put(this.keyPrefix + PB_KEY, roundedCpm); keywordMap.put(this.keyPrefix + BIDDER_KEY, bidder); diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java index 823c29d5bd2..480f8c9dd49 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -42,6 +42,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; import org.prebid.server.util.ObjectUtil; import java.math.BigDecimal; @@ -83,6 +84,7 @@ public BasicCategoryMappingService(ApplicationSettings applicationSettings, Jack @Override public Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout) { final ExtRequestTargeting targeting = targeting(bidRequest); @@ -110,9 +112,21 @@ public Future createCategoryMapping(List final List rejectedBids = new ArrayList<>(); return makeBidderToBidCategory( - bidderResponses, withCategory, translateCategories, primaryAdServer, publisher, rejectedBids, timeout) + bidderResponses, + withCategory, + translateCategories, + primaryAdServer, + publisher, + rejectedBids, + timeout) .map(categoryBidContexts -> resolveBidsCategoriesDurations( - bidderResponses, categoryBidContexts, bidRequest, targeting, withCategory, rejectedBids)); + bidderResponses, + categoryBidContexts, + account, + bidRequest, + targeting, + withCategory, + rejectedBids)); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { @@ -326,6 +340,7 @@ private static void collectCategoryFetchResults(CompositeFuture compositeFuture, */ private CategoryMappingResult resolveBidsCategoriesDurations(List bidderResponses, List categoryBidContexts, + Account account, BidRequest bidRequest, ExtRequestTargeting targeting, boolean withCategory, @@ -342,8 +357,15 @@ private CategoryMappingResult resolveBidsCategoriesDurations(List> uniqueCatKeysToCategoryBids = categoryBidContexts.stream() - .map(categoryBidContext -> enrichCategoryBidContext(categoryBidContext, durations, priceGranularity, - withCategory, appendBidderNames, impIdToBiddersDealTear, rejectedBids)) + .map(categoryBidContext -> enrichCategoryBidContext( + categoryBidContext, + account, + durations, + priceGranularity, + withCategory, + appendBidderNames, + impIdToBiddersDealTear, + rejectedBids)) .filter(Objects::nonNull) .collect(Collectors.groupingBy(CategoryBidContext::getCategoryUniqueKey, Collectors.mapping(Function.identity(), Collectors.toSet()))); @@ -504,6 +526,7 @@ private static boolean isNotRejected(String bidId, String bidder, List durations, PriceGranularity priceGranularity, boolean withCategory, @@ -522,7 +545,7 @@ private CategoryBidContext enrichCategoryBidContext(CategoryBidContext categoryB return null; } - final BigDecimal price = CpmRange.fromCpmAsNumber(bid.getPrice(), priceGranularity); + final BigDecimal price = CpmRange.fromCpmAsNumber(bid.getPrice(), priceGranularity, account); final String rowPrice = CpmRange.format(price, priceGranularity.getPrecision()); final String category = categoryBidContext.getCategory(); final String categoryUniqueKey = createCategoryUniqueKey(withCategory, category, rowPrice, duration); diff --git a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java index 2c3b3f369b0..e99f05dd815 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java @@ -5,6 +5,7 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; import java.util.List; @@ -12,5 +13,6 @@ public interface CategoryMappingService { Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java index 88ac988c521..4bf54857955 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java @@ -5,6 +5,7 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; import java.util.List; @@ -13,6 +14,7 @@ public class NoOpCategoryMappingService implements CategoryMappingService { @Override public Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout) { return Future.succeededFuture(CategoryMappingResult.of(bidderResponses)); diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java new file mode 100644 index 00000000000..839602540aa --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java @@ -0,0 +1,20 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AccountAuctionBidRoundingMode { + + @JsonProperty("down") + @JsonEnumDefaultValue + DOWN, + + @JsonProperty("true") + TRUE, + + @JsonProperty("timesplit") + TIMESPLIT, + + @JsonProperty("up") + UP +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index 96e12b0e927..82bf01afb75 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -45,6 +45,9 @@ public class AccountAuctionConfig { AccountTargetingConfig targeting; + @JsonAlias("bid-rounding") + AccountAuctionBidRoundingMode bidRounding; + @JsonProperty("preferredmediatype") Map preferredMediaTypes; 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 984ef6409b2..d0b3ee586d2 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 @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.request.auction.BidAdjustment +import org.prebid.server.functional.model.request.auction.BidRounding import org.prebid.server.functional.model.request.auction.PaaFormat import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.MediaType @@ -32,6 +33,7 @@ class AccountAuctionConfig { PrivacySandbox privacySandbox @JsonProperty("bidadjustments") BidAdjustment bidAdjustments + BidRounding bidRounding @JsonProperty("price_granularity") PriceGranularityType priceGranularitySnakeCase @@ -49,4 +51,6 @@ class AccountAuctionConfig { AccountBidValidationConfig bidValidationsSnakeCase @JsonProperty("price_floors") AccountPriceFloorsConfig priceFloorsSnakeCase + @JsonProperty("bid_rounding") + BidRounding bidRoundingSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy new file mode 100644 index 00000000000..ba612ce9f87 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum BidRounding { + + UP("up"), + DOWN("down"), + TRUE("true"), + TIME_SPLIT("timesplit"), + UNKNOWN("unknown"), + + private String value + + BidRounding(String value) { + this.value = value + } + + @Override + @JsonValue + String toString() { + return value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy index 78d6b03016d..fa72326fe7c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -86,7 +86,7 @@ class AmpSpec extends BaseSpec { then: "Response should contain information from stored response" def price = storedAuctionResponse.bid[0].price - assert response.targeting["hb_pb"] == getRoundedTargetingValueWithDefaultPrecision(price) + assert response.targeting["hb_pb"] == getRoundedTargetingValueWithDownPrecision(price) assert response.targeting["hb_size"] == "${storedAuctionResponse.bid[0].weight}x${storedAuctionResponse.bid[0].height}" and: "PBS not send request to bidder" diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index a4fda13f829..2943c78201a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -19,7 +19,11 @@ import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils import spock.lang.Specification +import java.math.RoundingMode + import static java.math.RoundingMode.DOWN +import static java.math.RoundingMode.HALF_UP +import static java.math.RoundingMode.UP import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.util.SystemProperties.DEFAULT_TIMEOUT @@ -81,8 +85,16 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { logs.findAll { it.contains(text) } } - protected static String getRoundedTargetingValueWithDefaultPrecision(BigDecimal value) { - "${value.setScale(DEFAULT_TARGETING_PRECISION, DOWN)}0" + protected static String getRoundedTargetingValueWithDownPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, DOWN) + } + + protected static String getRoundedTargetingValueWithHalfUpPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, HALF_UP) + } + + protected static String getRoundedTargetingValueWithUpPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, UP) } protected static Map> getRequests(BidResponse bidResponse) { @@ -101,4 +113,8 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { List bidderCalls) { [(bidderName): bidderCalls.collect { bidderCall -> decode(bidderCall.requestBody as String, BidderRequest) }] } + + private static GString roundWithDefaultPrecisionAndRoundingType(BigDecimal value, RoundingMode roundingMode) { + "${value.setScale(DEFAULT_TARGETING_PRECISION, roundingMode)}0" + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy new file mode 100644 index 00000000000..15095aaa3e8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy @@ -0,0 +1,111 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.request.auction.BidRounding.DOWN +import static org.prebid.server.functional.model.request.auction.BidRounding.TRUE +import static org.prebid.server.functional.model.request.auction.BidRounding.UNKNOWN +import static org.prebid.server.functional.model.request.auction.BidRounding.UP + +class BidRoundingSpec extends BaseSpec { + + def "PBS should round bid value to the down when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.randomFloorValue + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithDownPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: null), + new AccountAuctionConfig(bidRounding: UNKNOWN), + new AccountAuctionConfig(bidRounding: DOWN), + new AccountAuctionConfig(bidRoundingSnakeCase: DOWN)] + } + + def "PBS should round bid value to the up when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.getRandomFloorValue() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithUpPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: UP), + new AccountAuctionConfig(bidRoundingSnakeCase: UP)] + } + + def "PBS should round bid value to the up or down when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.getRandomFloorValue() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithHalfUpPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: TRUE), + new AccountAuctionConfig(bidRoundingSnakeCase: TRUE)] + } + + private static final Account getAccountWithBidRounding(String accountId, AccountAuctionConfig accountAuctionConfig) { + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy index 4d47947670a..cec8bb8fa41 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy @@ -77,7 +77,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { def response = floorsPbsService.sendAmpRequest(ampRequest) then: "PBS should suppress bids lower than floorRuleValue" - def bidPrice = getRoundedTargetingValueWithDefaultPrecision(floorValue) + def bidPrice = getRoundedTargetingValueWithDownPrecision(floorValue) verifyAll(response) { targeting["hb_pb_generic"] == bidPrice targeting["hb_pb"] == bidPrice diff --git a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java index 3c05d00af37..7be988f887a 100644 --- a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java +++ b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java @@ -32,6 +32,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.time.Clock; @@ -101,7 +102,7 @@ public void applyCategoryMappingShouldReturnFilteredBidsWithCategory() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -141,7 +142,7 @@ public void applyCategoryMappingShouldTolerateBidsWithSameIdWithingDifferentBidd // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -170,7 +171,7 @@ public void applyCategoryMappingShouldNotCallFetchCategoryWhenTranslateCategorie // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then verifyNoInteractions(applicationSettings); @@ -191,7 +192,7 @@ public void applyCategoryMappingShouldReturnFailedFutureWhenTranslateTrueAndAdSe // when assertThatThrownBy(() -> categoryMappingService.createCategoryMapping( - bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), timeout)) + bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout)) .isInstanceOf(InvalidRequestException.class) .hasMessage("Primary ad server required but was not defined when translate category is enabled"); } @@ -207,7 +208,7 @@ public void applyCategoryMappingShouldReturnFailedFutureWhenTranslateTrueAndAdSe // when and then assertThatThrownBy(() -> categoryMappingService.createCategoryMapping( - bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), timeout)) + bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout)) .isInstanceOf(InvalidRequestException.class) .hasMessage("Primary ad server `3` is not recognized"); } @@ -226,7 +227,10 @@ public void applyCategoryMappingShouldReturnUseFreewheelAdServerWhenAdServerIs1( Future.succeededFuture(singletonMap("cat1", "fetchedCat1"))); // when - categoryMappingService.createCategoryMapping(bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), + categoryMappingService.createCategoryMapping( + bidderResponses, + givenBidRequestWithTargeting(extRequestTargeting), + Account.empty("id"), timeout); // then @@ -247,7 +251,10 @@ public void applyCategoryMappingShouldReturnUseDpfAdServerWhenAdServerIs2() { Future.succeededFuture(singletonMap("cat1", "fetchedCat1"))); // when - categoryMappingService.createCategoryMapping(bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), + categoryMappingService.createCategoryMapping( + bidderResponses, + givenBidRequestWithTargeting(extRequestTargeting), + Account.empty("id"), timeout); // then @@ -271,7 +278,7 @@ public void applyCategoryMappingShouldRejectBidsWithFailedCategoryFetch() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -298,7 +305,7 @@ public void applyCategoryMappingShouldRejectBidsWithCatLengthMoreThanOne() { Future.succeededFuture(singletonMap("cat2", "fetchedCat2"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -326,7 +333,7 @@ public void applyCategoryMappingShouldRejectBidsWithWhenCatIsNull() { Future.succeededFuture(singletonMap("cat2", "fetchedCat2"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -354,7 +361,7 @@ public void applyCategoryMappingShouldRejectBidWhenNullCategoryReturnedFromSourc Future.succeededFuture(null)); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -385,7 +392,7 @@ public void applyCategoryMappingShouldUseMediaTypePriceGranularityIfDefined() { Future.succeededFuture(singletonMap("cat1", "fetchedCat1"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -410,7 +417,7 @@ public void applyCategoryMappingShouldRejectBidIfItsDurationLargerThanTargetingM Future.succeededFuture(singletonMap("cat2", "fetchedCat2"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -433,7 +440,7 @@ public void applyCategoryMappingShouldReturnEmptyCategoryMappingResult() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -462,7 +469,7 @@ public void applyCategoryMappingShouldReturnFirstVideoCategoryIfPresent() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -487,7 +494,7 @@ public void applyCategoryMappingShouldReturnEmptyCategoryIfNotWithCategory() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -512,7 +519,7 @@ public void applyCategoryMappingShouldReturnFirstIabBidCategoryIfWithCategoryAnd // when final Future resultFuture = categoryMappingService.createCategoryMapping( - bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), timeout); + bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -540,7 +547,7 @@ public void applyCategoryMappingShouldReturnFetchedCategoryIfWithCategoryAndTran // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -562,7 +569,7 @@ public void applyCategoryMappingShouldSetFirstDurationFromRangeIfDurationIsNull( // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -588,7 +595,7 @@ public void applyCategoryMappingShouldDeduplicateBidsByFetchedCategoryWhenWithCa // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -615,7 +622,7 @@ public void applyCategoryMappingShouldDeduplicateBidsByBidCatWhenWithCategoryIsT // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -643,7 +650,7 @@ public void applyCategoryMappingShouldDeduplicateBidsByPriceAndDurationIfWithCat // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -667,7 +674,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndFetchedCatego // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -689,7 +696,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndBidCatAndDura // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -712,7 +719,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndBidCatAndDura // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -734,7 +741,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndDuration() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -764,7 +771,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriorityAndDuration() // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -801,7 +808,7 @@ public void applyCategoryMappingShouldUseDealTierFromImpExtPrebidBidders() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -840,7 +847,7 @@ public void applyCategoryMappingShouldPrecedencePriorityAndDurationFromPrebidOve // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -875,7 +882,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriorityCatAndDuratio // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -912,7 +919,7 @@ public void applyCategoryMappingShouldBuildCatDurFromPriceCatAndDurIfSupportDeal // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -947,7 +954,7 @@ public void applyCategoryMappingShouldBuildCatDurFromPriceCatAndDurIfBidPriority // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -987,7 +994,7 @@ public void applyCategoryMappingShouldIgnoreContextAndPrebidInImpExt() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1022,7 +1029,7 @@ public void applyCategoryMappingShouldAddErrorIfImpBidderDoesNotHaveDealTierAndC // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1060,7 +1067,7 @@ public void applyCategoryMappingShouldAddErrorIfPrefixIsNullAndCreateRegularCatD // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1099,7 +1106,7 @@ public void applyCategoryMappingShouldAddErrorIfMinDealTierIsNullAndCreateRegula // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1136,7 +1143,7 @@ public void applyCategoryMappingShouldAddErrorIfMinDealTierLessThanZeroAndCreate // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1167,7 +1174,7 @@ public void applyCategoryMappingShouldRejectAllBidsFromBidderInDifferentReasons( // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 7410c387a22..f62c6a44651 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -229,7 +229,7 @@ public void setUp() { given(cacheDefaultProperties.getAudioTtl()).willReturn(null); given(cacheDefaultProperties.getNativeTtl()).willReturn(null); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture( CategoryMappingResult.of(emptyMap(), emptyMap(), invocationOnMock.getArgument(0), null))); @@ -1102,7 +1102,7 @@ public void shouldUseBidsReturnedInCategoryMapperResultAndUpdateErrors() { bidRequest, contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willReturn(Future.succeededFuture(CategoryMappingResult.of(emptyMap(), emptyMap(), singletonList(BidderResponse.of( "bidder1", @@ -1133,7 +1133,7 @@ public void shouldThrowExceptionWhenCategoryMappingThrowsPrebidException() { givenBidRequest(identity(), identity(), givenImp()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willReturn(Future.failedFuture(new InvalidRequestException("category exception"))); // when @@ -2456,7 +2456,7 @@ public void shouldAddDealTierSatisfiedToExtBidPrebidWhenBidsPrioritySatisfiedMin givenBidRequest(Imp.builder().id("i1").build()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willReturn(Future.succeededFuture(CategoryMappingResult.of(emptyMap(), Collections.singletonMap(bid, true), bidderResponses, emptyList()))); diff --git a/src/test/java/org/prebid/server/auction/CpmRangeTest.java b/src/test/java/org/prebid/server/auction/CpmRangeTest.java index f3e72b5483d..48028175363 100644 --- a/src/test/java/org/prebid/server/auction/CpmRangeTest.java +++ b/src/test/java/org/prebid/server/auction/CpmRangeTest.java @@ -1,21 +1,29 @@ package org.prebid.server.auction; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionBidRoundingMode; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.auction.PriceGranularity.createFromExtPriceGranularity; +import static org.prebid.server.auction.PriceGranularity.createFromString; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.DOWN; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.TIMESPLIT; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.TRUE; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.UP; public class CpmRangeTest { @Test public void fromCpmShouldReturnMaxRangeIfCpmExceedsIt() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(21), PriceGranularity.createFromString("auto"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(21), createFromString("auto"), givenAccount())) .isEqualTo("20.00"); } @@ -27,7 +35,7 @@ public void fromCpmShouldReturnPriceWithCorrectPrecision() { ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(0.1))))); // when - final String cpm = CpmRange.fromCpm(BigDecimal.valueOf(5.1245), priceGranularity); + final String cpm = CpmRange.fromCpm(BigDecimal.valueOf(5.1245), priceGranularity, givenAccount()); // then assertThat(cpm).isEqualTo("5.1"); @@ -35,88 +43,191 @@ public void fromCpmShouldReturnPriceWithCorrectPrecision() { @Test public void fromCpmShouldReturnCpmGivenLowGranularity() { - Assertions.assertThat( - CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("low"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount())) + .isEqualTo("3.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount(DOWN))) + .isEqualTo("3.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount(UP))) + .isEqualTo("4.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.74), createFromString("low"), givenAccount(UP))) + .isEqualTo("4.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount(TRUE))) + .isEqualTo("4.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.74), createFromString("low"), givenAccount(TRUE))) .isEqualTo("3.50"); } @Test public void fromCpmShouldReturnCpmGivenMedGranularity() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("med"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount())) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount(DOWN))) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("med"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount(TRUE))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("med"), givenAccount(TRUE))) .isEqualTo("3.80"); - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("medium"))) + + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount())) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount(DOWN))) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("medium"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount(TRUE))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("medium"), givenAccount(TRUE))) .isEqualTo("3.80"); } @Test public void fromCpmShouldReturnCpmGivenHighGranularity() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("high"))) - .isEqualTo("3.87"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount())) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(DOWN))) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(UP))) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("high"), givenAccount(UP))) + .isEqualTo("3.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(TRUE))) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("high"), givenAccount(TRUE))) + .isEqualTo("3.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(TIMESPLIT))) + .isEqualTo("3.85"); + } @Test public void fromCpmShouldReturnCpmGivenAutoGranularityAndFirstRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("auto"))) - .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount())) + .isEqualTo("3.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("3.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount(UP))) + .isEqualTo("3.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.32), createFromString("dense"), givenAccount(UP))) + .isEqualTo("3.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("3.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.32), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("3.30"); } @Test public void fromCpmShouldReturnCpmGivenAutoGranularityAndSecondRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.32), PriceGranularity.createFromString("auto"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount())) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount(DOWN))) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount(UP))) + .isEqualTo("5.40"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.34), createFromString("auto"), givenAccount(UP))) + .isEqualTo("5.40"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount(TRUE))) + .isEqualTo("5.40"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.34), createFromString("auto"), givenAccount(TRUE))) .isEqualTo("5.30"); } @Test public void fromCpmShouldReturnCpmGivenAutoGranularityAndThirdRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.59), PriceGranularity.createFromString("auto"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount())) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount(DOWN))) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("auto"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount(TRUE))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("auto"), givenAccount(TRUE))) .isEqualTo("13.50"); } @Test public void fromCpmShouldReturnCpmGivenDenseGranularityAndFirstRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.87), PriceGranularity.createFromString("dense"))) - .isEqualTo("2.87"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount())) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(UP))) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.84), createFromString("dense"), givenAccount(UP))) + .isEqualTo("2.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.84), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("2.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(TIMESPLIT))) + .isEqualTo("2.85"); } @Test public void fromCpmShouldReturnCpmGivenDenseGranularityAndSecondRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.36), PriceGranularity.createFromString("dense"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount())) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount(UP))) .isEqualTo("5.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.32), createFromString("dense"), givenAccount(UP))) + .isEqualTo("5.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("5.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.32), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("5.30"); } @Test public void fromCpmShouldReturnCpmGivenDenseGranularityAndThirdRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.69), PriceGranularity.createFromString("dense"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount())) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("dense"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("dense"), givenAccount(TRUE))) .isEqualTo("13.50"); } @Test public void fromCpmShouldReturnResultWithDefaultPrecisionTwoIfRangePrecisionInNull() { - Assertions.assertThat( - CpmRange.fromCpm(BigDecimal.valueOf(2.3333), PriceGranularity.createFromExtPriceGranularity( + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.3333), createFromExtPriceGranularity( ExtPriceGranularity.of(null, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), - BigDecimal.valueOf(0.01))))))) + BigDecimal.valueOf(0.01))))), givenAccount())) .isEqualTo("2.33"); } @Test public void fromCpmShouldReturnResultWithPrecisionZero() { - Assertions.assertThat( - CpmRange.fromCpm(BigDecimal.valueOf(2.3333), PriceGranularity.createFromExtPriceGranularity( + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.3333), createFromExtPriceGranularity( ExtPriceGranularity.of(0, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), - BigDecimal.valueOf(0.01))))))) + BigDecimal.valueOf(0.01))))), givenAccount())) .isEqualTo("2"); } @Test public void fromCpmAsNumberShouldReturnExpectedResult() { // given - final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity( + final PriceGranularity priceGranularity = createFromExtPriceGranularity( ExtPriceGranularity.of(null, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), BigDecimal.valueOf(0.01))))); // when - final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2.333), priceGranularity); + final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2.333), priceGranularity, givenAccount()); // then assertThat(result.compareTo(BigDecimal.valueOf(2.33))).isEqualTo(0); @@ -125,13 +236,13 @@ public void fromCpmAsNumberShouldReturnExpectedResult() { @Test public void fromCpmAsNumberShouldReturnExpectedResultForMultipleRanges() { // given - final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity( + final PriceGranularity priceGranularity = createFromExtPriceGranularity( ExtPriceGranularity.of(2, asList( ExtGranularityRange.of(BigDecimal.valueOf(1.5), BigDecimal.ONE), ExtGranularityRange.of(BigDecimal.valueOf(2.5), BigDecimal.valueOf(1.2))))); // when - final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2), priceGranularity); + final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2), priceGranularity, givenAccount()); // then assertThat(result.compareTo(BigDecimal.valueOf(1.5))).isEqualTo(0); @@ -140,14 +251,22 @@ public void fromCpmAsNumberShouldReturnExpectedResultForMultipleRanges() { @Test public void fromCpmAsNumberShouldRetunNullIfPriceDoesNotFitToRange() { // given - final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity( + final PriceGranularity priceGranularity = createFromExtPriceGranularity( ExtPriceGranularity.of(null, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), BigDecimal.valueOf(0.01))))); // when - final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(-2.0), priceGranularity); + final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(-2.0), priceGranularity, givenAccount()); // then assertThat(result).isNull(); } + + private static Account givenAccount(AccountAuctionBidRoundingMode mode) { + return Account.builder().auction(AccountAuctionConfig.builder().bidRounding(mode).build()).build(); + } + + private static Account givenAccount() { + return Account.builder().build(); + } } diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index 879bd7873c2..49ab351205a 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -6,6 +6,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.util.Map; @@ -45,7 +46,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", false, null, null, null, null); + .makeFor(bid, "bidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsOnly( @@ -76,7 +77,7 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "veryververyverylongbidder1", false, null, null, null, null); + .makeFor(bid, "veryververyverylongbidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsOnly( @@ -111,7 +112,8 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, "cacheId1", "banner", "videoCacheId1", "categoryDuration"); + .makeFor(bid, "bidder1", true, "cacheId1", "banner", + "videoCacheId1", "categoryDuration", Account.empty("accountId")); // then assertThat(keywords).containsOnly( @@ -154,7 +156,7 @@ public void shouldIncludeFormatOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "", true, null, "banner", null, null); + .makeFor(bid, "", true, null, "banner", null, null, Account.empty("accountId")); // then assertThat(keywords).contains(entry("hb_format", "banner")); @@ -180,7 +182,7 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_cache_id_bidder", "hb_deal_bidder", "hb_size_bidder", @@ -207,7 +209,7 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).contains( @@ -235,7 +237,7 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_bidder", "hb_pb"); @@ -261,7 +263,7 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsKeys("hb_bidder", "hb_pb"); @@ -287,7 +289,7 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_bidder_bidder1", "hb_pb_bidder1"); @@ -313,7 +315,7 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsKeys("hb_bidder_bidder1", "hb_pb_bidder1"); @@ -339,7 +341,7 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() null, null, defaultKeyPrefix) - .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null); + .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).hasSize(2) @@ -366,7 +368,7 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).hasSize(2) @@ -393,7 +395,7 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then // Without truncating: "hb_bidder", "hb_bidder_bidder", "hb_env", "hb_env_bidder", "hb_pb", "hb_pb_bidder" @@ -421,7 +423,7 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() null, null, defaultKeyPrefix) - .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null); + .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).hasSize(2) @@ -454,7 +456,7 @@ public void shouldTruncateKeysFromResolver() { null, resolver, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).contains(entry("key_longer_than_twen", "value1")); @@ -486,7 +488,7 @@ public void shouldIncludeKeywordsFromResolver() { null, resolver, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).contains(entry("keyword1", "value1")); @@ -512,7 +514,7 @@ public void shouldIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsTrue() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", false, null, null, null, null); + .makeFor(bid, "bidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsOnlyKeys("hb_bidder_bidder1", "hb_deal_bidder1", "hb_pb_bidder1"); @@ -538,7 +540,7 @@ public void shouldNotIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsFalse() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", false, null, null, null, null); + .makeFor(bid, "bidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_bidder_bidder1", "hb_deal_bidder1", "hb_pb_bidder1"); From df8f69dd338708cc98e59f1f9affc9863eb7e81d Mon Sep 17 00:00:00 2001 From: andre-gielow-ttd <124626380+andre-gielow-ttd@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:06:18 -0400 Subject: [PATCH 14/51] Add ttd/thetradedesk alias (#3979) --- src/main/resources/bidder-config/thetradedesk.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/bidder-config/thetradedesk.yaml b/src/main/resources/bidder-config/thetradedesk.yaml index b71a0e38cc2..e8b9976e786 100644 --- a/src/main/resources/bidder-config/thetradedesk.yaml +++ b/src/main/resources/bidder-config/thetradedesk.yaml @@ -1,6 +1,8 @@ adapters: thetradedesk: endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}} + aliases: + ttd: ~ meta-info: maintainer-email: Prebid-Maintainers@thetradedesk.com app-media-types: From 1bcd9eb35e9b1d300b62a176d98a33629b4c9887 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 4 Jun 2025 14:06:30 +0200 Subject: [PATCH 15/51] 51Degrees module: update `devicetype` mapping. (#3978) --- .../fiftyone/devicedetection/v1/core/OrtbDeviceType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java index a31d387c991..fc5a6c8d9ed 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java @@ -27,7 +27,7 @@ public enum OrtbDeviceType { Map.entry("Mobile", OrtbDeviceType.MOBILE_TABLET), Map.entry("Router", OrtbDeviceType.CONNECTED_DEVICE), Map.entry("SmallScreen", OrtbDeviceType.CONNECTED_DEVICE), - Map.entry("SmartPhone", OrtbDeviceType.MOBILE_TABLET), + Map.entry("SmartPhone", OrtbDeviceType.PHONE), Map.entry("SmartSpeaker", OrtbDeviceType.CONNECTED_DEVICE), Map.entry("SmartWatch", OrtbDeviceType.CONNECTED_DEVICE), Map.entry("Tablet", OrtbDeviceType.TABLET), From 7e2cacfc956ea3f6623bbfc534f434b73e093c48 Mon Sep 17 00:00:00 2001 From: Marc-Enzo Bonnafon Date: Wed, 4 Jun 2025 13:06:58 +0100 Subject: [PATCH 16/51] Port Mobkoi: New Adapter (#3942) --- .../server/bidder/mobkoi/MobkoiBidder.java | 154 +++++++++++ .../ext/request/mobkoi/ExtImpMobkoi.java | 14 + .../config/bidder/MobkoiConfiguration.java | 41 +++ src/main/resources/bidder-config/mobkoi.yaml | 11 + .../static/bidder-params/mobkoi.json | 17 ++ .../bidder/mobkoi/MobkoiBidderTest.java | 261 ++++++++++++++++++ .../java/org/prebid/server/it/MobkoiTest.java | 34 +++ .../mobkoi/test-auction-mobkoi-request.json | 23 ++ .../mobkoi/test-auction-mobkoi-response.json | 40 +++ .../mobkoi/test-mobkoi-bid-request.json | 55 ++++ .../mobkoi/test-mobkoi-bid-response.json | 18 ++ .../server/it/test-application.properties | 2 + 12 files changed, 670 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/MobkoiConfiguration.java create mode 100644 src/main/resources/bidder-config/mobkoi.yaml create mode 100644 src/main/resources/static/bidder-params/mobkoi.json create mode 100644 src/test/java/org/prebid/server/bidder/mobkoi/MobkoiBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/MobkoiTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java b/src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java new file mode 100644 index 00000000000..97cd481b235 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java @@ -0,0 +1,154 @@ +package org.prebid.server.bidder.mobkoi; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.mobkoi.ExtImpMobkoi; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class MobkoiBidder implements Bidder { + + private static final TypeReference> MOBKOI_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MobkoiBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final Imp firstImp = bidRequest.getImp().getFirst(); + + final ExtImpMobkoi extImpMobkoi; + final Imp modifiedFirstImp; + try { + extImpMobkoi = parseExtImp(firstImp); + modifiedFirstImp = modifyImp(firstImp, extImpMobkoi); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final String selectedEndpointUrl = resolveEndpoint(extImpMobkoi.getAdServerBaseUrl()); + + return Result.withValue(BidderUtil.defaultRequest( + modifyBidRequest(bidRequest, modifiedFirstImp), + selectedEndpointUrl, + mapper)); + } + + private ExtImpMobkoi parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), MOBKOI_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext for impression id %s. Error Information: %s" + .formatted(imp.getId(), e.getMessage())); + } + } + + private Imp modifyImp(Imp firstImp, ExtImpMobkoi extImpMobkoi) { + if (StringUtils.isNotBlank(firstImp.getTagid())) { + return firstImp; + } + + if (StringUtils.isNotBlank(extImpMobkoi.getPlacementId())) { + return firstImp.toBuilder().tagid(extImpMobkoi.getPlacementId()).build(); + } + + throw new PreBidException("invalid because it comes with neither request.imp[0].tagId nor " + + "req.imp[0].ext.Bidder.placementId"); + } + + // url is already validated with `bidder-params` json schema + private String resolveEndpoint(String customUri) { + if (customUri == null) { + return endpointUrl; + } + try { + final URI uri = new URI(customUri); + return uri.resolve("/bid").toString(); + } catch (IllegalArgumentException | URISyntaxException e) { + return endpointUrl; + } + } + + private static BidRequest modifyBidRequest(BidRequest bidRequest, Imp modifiedFirstImp) { + final User user = modifyUser(bidRequest.getUser()); + final List imps = updateFirstImpWith(bidRequest.getImp(), modifiedFirstImp); + return bidRequest.toBuilder().user(user).imp(imps).build(); + } + + private static User modifyUser(User user) { + return Optional.ofNullable(user) + .map(User::getConsent) + .map(consent -> ExtUser.builder().consent(consent).build()) + .map(ext -> user.toBuilder().ext(ext).build()) + .orElse(user); + } + + private static List updateFirstImpWith(List imps, Imp imp) { + final List modifiedImps = new ArrayList<>(imps); + modifiedImps.set(0, imp); + return Collections.unmodifiableList(modifiedImps); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse); + } + + private static List bidsFromResponse(BidResponse bidResponse) { + return bidResponse.getSeatbid() + .stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> BidderBid.of(bid, BidType.banner, "mobkoi", bidResponse.getCur())) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java new file mode 100644 index 00000000000..0faa3d1efc8 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.mobkoi; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMobkoi { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("adServerBaseUrl") + String adServerBaseUrl; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MobkoiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MobkoiConfiguration.java new file mode 100644 index 00000000000..9b4761fdfaf --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MobkoiConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.mobkoi.MobkoiBidder; +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/mobkoi.yaml", factory = YamlPropertySourceFactory.class) +public class MobkoiConfiguration { + + private static final String BIDDER_NAME = "mobkoi"; + + @Bean("mobkoiConfigurationProperties") + @ConfigurationProperties("adapters.mobkoi") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps mobkoiBidderDeps(BidderConfigurationProperties mobkoiConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(mobkoiConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MobkoiBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/mobkoi.yaml b/src/main/resources/bidder-config/mobkoi.yaml new file mode 100644 index 00000000000..cccf3822bf3 --- /dev/null +++ b/src/main/resources/bidder-config/mobkoi.yaml @@ -0,0 +1,11 @@ +adapters: + mobkoi: + endpoint: "https://pbs.maximus.mobkoi.com/bid" + ortb-version: "2.6" + meta-info: + maintainer-email: platformteam@mobkoi.com + app-media-types: + site-media-types: + - banner + supported-vendors: + vendor-id: 898 diff --git a/src/main/resources/static/bidder-params/mobkoi.json b/src/main/resources/static/bidder-params/mobkoi.json new file mode 100644 index 00000000000..8286229b035 --- /dev/null +++ b/src/main/resources/static/bidder-params/mobkoi.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mobkoi Adapter Params", + "description": "A schema which validates params accepted by the Mobkoi adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "Placement ID" + }, + "adServerBaseUrl": { + "type": "string", + "description": "Mobkoi's ad server url", + "pattern": "^https?://[^.]+\\.mobkoi\\.com$" + } + } +} diff --git a/src/test/java/org/prebid/server/bidder/mobkoi/MobkoiBidderTest.java b/src/test/java/org/prebid/server/bidder/mobkoi/MobkoiBidderTest.java new file mode 100644 index 00000000000..1c98d21a3ca --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/mobkoi/MobkoiBidderTest.java @@ -0,0 +1,261 @@ +package org.prebid.server.bidder.mobkoi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +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.VertxTest; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.mobkoi.ExtImpMobkoi; + +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badInput; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; + +public class MobkoiBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com/bid"; + + private final MobkoiBidder target = new MobkoiBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new MobkoiBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenRequestHasInvalidExtImpression() { + // given + final ObjectNode invalidExt = mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(invalidExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).first() + .satisfies(error -> { + assertThat(error.getMessage()) + .startsWith("Invalid imp.ext for impression id imp_id. Error Information:"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenRequestHasMissingTagIdAndPlacementId() { + // given + final ObjectNode mobkoiExt = impExt(null, null); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mobkoiExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()) + .containsExactly( + badInput("invalid because it comes with neither " + + "request.imp[0].tagId nor req.imp[0].ext.Bidder.placementId")); + } + + @Test + public void makeHttpRequestsShouldAddPlacementIdOnlyInFirstImpressionTagId() { + // given + final ObjectNode mobkoiExt = impExt("pid", null); + final Imp givenImp1 = givenImp(impBuilder -> impBuilder.ext(mobkoiExt)); + final Imp givenImp2 = givenImp(identity()); + final BidRequest bidRequest = BidRequest.builder().imp(asList(givenImp1, givenImp2)).build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp) + .extracting(imp -> imp.getTagid()) + .containsExactly("pid", null); + } + + @Test + public void makeHttpRequestsShouldUseConstructorEndpointWhenNoCustomEndpointIsDefinedInMobkoiExtension() { + // given + final ObjectNode mobkoiExt = impExt("pid", null); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mobkoiExt)); + + // when + final Result>> results = target.makeHttpRequests(bidRequest); + + // then + assertThat(results.getValue()).extracting(HttpRequest::getUri).containsExactly("https://test.endpoint.com/bid"); + assertThat(results.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldConstructWithDefaultEndpointWhenTheCustomURLIsInvalidInMobkoiExtension() { + // given + final ObjectNode mobkoiExt = impExt("pid", "invalid URI"); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mobkoiExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).extracting(HttpRequest::getUri).containsExactly("https://test.endpoint.com/bid"); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldUseCustomEndpointWhenDefinedInMobkoiExtension() { + // given + final ObjectNode mobkoiExt = impExt("pid", "https://custom.endpoint.com"); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(mobkoiExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).extracting(HttpRequest::getUri).containsExactly("https://custom.endpoint.com/bid"); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldOverrideUserExtAndSetConsent() { + // given + final ObjectNode originExtUserData = mapper.createObjectNode().put("originAttr", "originValue"); + final ExtUser extUser = ExtUser.builder().data(originExtUserData).consent("consent-to-be-overridden").build(); + final User user = User.builder().consent("consent").ext(extUser).build(); + final BidRequest bidRequest = givenBidRequest( + bidRequestBuilder -> bidRequestBuilder.user(user), + impBuilder -> impBuilder); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .extracting(request -> request.getUser().getExt()).hasSize(1).element(0) + .isEqualTo(ExtUser.builder() + .consent("consent") + .build()); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + }); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWithMobkoiSeat() throws JsonProcessingException { + // given + final BidResponse bannerBidResponse = givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123")); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bannerBidResponse)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly( + BidderBid.of( + Bid.builder().mtype(1).impid("123").build(), banner, "mobkoi", "USD")); + } + + private static BidRequest givenBidRequest(UnaryOperator impModifier) { + return BidRequest.builder() + .imp(singletonList(givenImp(impModifier))) + .build(); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impModifier) { + return bidRequestCustomizer.apply(BidRequest.builder()) + .imp(singletonList(givenImp(impModifier))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("imp_id").ext(impExt("placementIdValue", null))).build(); + } + + private static ObjectNode impExt(String placementId, String adServerBaseUrl) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpMobkoi.of(placementId, adServerBaseUrl))); + } + + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } +} + diff --git a/src/test/java/org/prebid/server/it/MobkoiTest.java b/src/test/java/org/prebid/server/it/MobkoiTest.java new file mode 100644 index 00000000000..1246f6d82f2 --- /dev/null +++ b/src/test/java/org/prebid/server/it/MobkoiTest.java @@ -0,0 +1,34 @@ +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 MobkoiTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromMobkoi() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/mobkoi-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/mobkoi/test-mobkoi-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/mobkoi/test-mobkoi-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/mobkoi/test-auction-mobkoi-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals("openrtb2/mobkoi/test-auction-mobkoi-response.json", response, singletonList("mobkoi")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-request.json new file mode 100644 index 00000000000..42d9bb8580c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "mobkoi": { + "placementId": "999999" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-response.json new file mode 100644 index 00000000000..a791e359cb7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-auction-mobkoi-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "mtype": 1, + "price": 0.01, + "adm": "
", + "cid": "test_cid", + "crid": "test_banner_crid", + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "mobkoi" + } + }, + "origbidcpm": 0.01 + } + } + ], + "group": 0, + "seat": "mobkoi" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "mobkoi": "{{ mobkoi.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-request.json new file mode 100644 index 00000000000..f0baad1ac99 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-request.json @@ -0,0 +1,55 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "tagid" : "999999", + "secure": 1, + "ext": { + "tid" :"${json-unit.any-string}", + "bidder": { + "placementId": "999999" + } + } + } + ], + "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" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "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/mobkoi/test-mobkoi-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-response.json new file mode 100644 index 00000000000..34c58bd8a52 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobkoi/test-mobkoi-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "adm": "
", + "crid": "test_banner_crid", + "cid": "test_cid", + "impid": "imp_id", + "id": "bid_id", + "mtype": 1, + "price": 0.01 + } + ] + } + ] +} 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 44159e6e071..5216ec30fc8 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -359,6 +359,8 @@ adapters.mobfoxpb.enabled=true adapters.mobfoxpb.endpoint=http://localhost:8090/mobfoxpb-exchange?c=__route__&m=__method__&key=__key__ adapters.mobilefuse.enabled=true adapters.mobilefuse.endpoint=http://localhost:8090/mobilefuse-exchange/ +adapters.mobkoi.enabled=true +adapters.mobkoi.endpoint=http://localhost:8090/mobkoi-exchange adapters.motorik.enabled=true adapters.motorik.endpoint=http://localhost:8090/motorik-exchange?k={{AccountID}}&name={{SourceId}} adapters.nextmillennium.enabled=true From 36e0e3b771c667653a8e78261ea49e03088a687c Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Fri, 6 Jun 2025 15:45:59 +0300 Subject: [PATCH 17/51] Core: Update exception handler log filter (#3986) --- src/main/java/org/prebid/server/handler/ExceptionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/prebid/server/handler/ExceptionHandler.java b/src/main/java/org/prebid/server/handler/ExceptionHandler.java index f56a21d8e5c..1bff922f5c3 100644 --- a/src/main/java/org/prebid/server/handler/ExceptionHandler.java +++ b/src/main/java/org/prebid/server/handler/ExceptionHandler.java @@ -38,7 +38,7 @@ private static boolean shouldLogException(Throwable exception) { private static boolean isConnectionResetException(Throwable exception) { return exception instanceof IOException - && StringUtils.equals("readAddress(..) failed: Connection reset by peer", exception.getMessage()); + && StringUtils.equals("recvAddress(..) failed: Connection reset by peer", exception.getMessage()); } private static String errorMessageFrom(Throwable exception) { From a0e5ee83c0796a0f36a3467caadb4bbafa9902b7 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:46:09 +0200 Subject: [PATCH 18/51] Missena Adapter: Add formats and settings params (#3970) --- .../bidder/missena/MissenaAdRequest.java | 48 +++- .../server/bidder/missena/MissenaBidder.java | 128 +++++++-- .../bidder/missena/MissenaUserParams.java | 23 ++ .../ext/request/missena/ExtImpMissena.java | 11 +- .../config/bidder/MissenaConfiguration.java | 7 +- src/main/resources/bidder-config/missena.yaml | 2 +- .../static/bidder-params/missena.json | 11 + .../bidder/missena/MissenaBidderTest.java | 259 ++++++++++++------ .../org/prebid/server/it/MissenaTest.java | 2 - .../missena/test-missena-bid-request.json | 21 +- 10 files changed, 384 insertions(+), 128 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java index 80bcba81fc6..68c0d862681 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -1,25 +1,59 @@ package org.prebid.server.bidder.missena; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.SupplyChain; import lombok.Builder; import lombok.Value; -@Builder(toBuilder = true) +import java.math.BigDecimal; +import java.util.List; + @Value +@Builder(toBuilder = true) public class MissenaAdRequest { - String requestId; + @JsonProperty("adunit") + String adUnit; + + @JsonProperty("buyeruid") + String buyerUid; + + Integer coppa; + + String currency; + + @JsonProperty("userEids") + List userEids; - int timeout; + BigDecimal floor; + + String floorCurrency; + + @JsonProperty("consent_required") + Boolean gdpr; + + @JsonProperty("consent_string") + String gdprConsent; + + @JsonProperty("ik") + String idempotencyKey; String referer; String refererCanonical; - String consentString; + String requestId; + + SupplyChain schain; + + Long timeout; + + String url; - boolean consentRequired; + MissenaUserParams params; - String placement; + String usPrivacy; - String test; + String version; } diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java index 913fd3b44b8..4cad9af88f7 100644 --- a/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -6,6 +6,7 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.vertx.core.MultiMap; @@ -16,7 +17,9 @@ import org.prebid.server.bidder.model.BidderCall; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -25,8 +28,13 @@ import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -37,31 +45,40 @@ public class MissenaBidder implements Bidder { private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; - private static final int AD_REQUEST_DEFAULT_TIMEOUT = 2000; + private static final String USD_CURRENCY = "USD"; + private static final String EUR_CURRENCY = "EUR"; + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; private final String endpointUrl; private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + private final PrebidVersionProvider prebidVersionProvider; + + public MissenaBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService, + PrebidVersionProvider prebidVersionProvider) { - public MissenaBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); } @Override public Result>> makeHttpRequests(BidRequest request) { - final List> requests = new ArrayList<>(); final List errors = new ArrayList<>(); for (Imp imp : request.getImp()) { try { final ExtImpMissena extImp = parseImpExt(imp); - requests.add(makeHttpRequest(request, imp.getId(), extImp)); + final HttpRequest httpRequest = makeHttpRequest(request, imp, extImp); + return Result.of(Collections.singletonList(httpRequest), errors); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - - return Result.of(requests, errors); + return Result.withErrors(errors); } private ExtImpMissena parseImpExt(Imp imp) { @@ -72,30 +89,90 @@ private ExtImpMissena parseImpExt(Imp imp) { } } - private HttpRequest makeHttpRequest(BidRequest request, String impId, ExtImpMissena extImp) { + private HttpRequest makeHttpRequest(BidRequest request, Imp imp, ExtImpMissena extImp) { final Site site = request.getSite(); + final User user = request.getUser(); + final Regs regs = request.getRegs(); + final Device device = request.getDevice(); + final Source source = request.getSource(); + + final String requestCurrency = resolveCurrency(request.getCur()); + final Price floorInfo = resolveBidFloor(imp, request, requestCurrency); + + final MissenaUserParams userParams = MissenaUserParams.builder() + .formats(extImp.getFormats()) + .placement(extImp.getPlacement()) + .testMode(extImp.getTestMode()) + .settings(extImp.getSettings()) + .build(); final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() + .adUnit(imp.getId()) + .buyerUid(user != null ? user.getBuyeruid() : null) + .coppa(regs != null ? regs.getCoppa() : null) + .currency(requestCurrency) + .userEids(user != null ? user.getEids() : null) + .floor(floorInfo.getValue()) + .floorCurrency(floorInfo.getCurrency()) + .gdpr(isGdpr(regs)) + .gdprConsent(getUserConsent(user)) + .idempotencyKey(request.getId()) + .referer(site != null ? site.getPage() : null) + .refererCanonical(site != null ? site.getDomain() : null) .requestId(request.getId()) - .timeout(AD_REQUEST_DEFAULT_TIMEOUT) - .referer(site == null ? null : site.getPage()) - .refererCanonical(site == null ? null : site.getDomain()) - .consentString(getUserConsent(request.getUser())) - .consentRequired(isGdpr(request.getRegs())) - .placement(extImp.getPlacement()) - .test(extImp.getTestMode()) + .schain(source != null ? source.getSchain() : null) + .timeout(request.getTmax()) + .params(userParams) + .version(prebidVersionProvider.getNameVersionRecord()) .build(); return HttpRequest.builder() .method(HttpMethod.POST) - .uri(makeUrl(extImp.getApiKey())) - .headers(makeHeaders(request.getDevice(), site)) - .impIds(Collections.singleton(impId)) + .uri(resolveEndpointUrl(extImp.getApiKey())) + .headers(makeHeaders(device, site)) + .impIds(Collections.singleton(imp.getId())) .body(mapper.encodeToBytes(missenaAdRequest)) .payload(missenaAdRequest) .build(); } + private Price resolveBidFloor(Imp imp, BidRequest bidRequest, String targetCurrency) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.isValidPrice(initialBidFloorPrice) + ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest, targetCurrency) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest, String targetCurrency) { + final String bidFloorCur = bidFloorPrice.getCurrency(); + + try { + final BigDecimal convertedPrice = currencyConversionService + .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, targetCurrency); + + return Price.of(targetCurrency, convertedPrice); + } catch (PreBidException e) { + throw new PreBidException("Unable to convert provided bid floor currency from %s to %s for imp `%s`" + .formatted(bidFloorCur, targetCurrency, impId)); + } + } + + private String resolveCurrency(List requestCurrencies) { + String currency = USD_CURRENCY; + + for (String requestCurrency : requestCurrencies) { + if (USD_CURRENCY.equalsIgnoreCase(requestCurrency)) { + return USD_CURRENCY; + } + + if (EUR_CURRENCY.equalsIgnoreCase(requestCurrency)) { + currency = EUR_CURRENCY; + } + } + + return currency; + } + private MultiMap makeHeaders(Device device, Site site) { final MultiMap headers = HttpUtil.headers(); @@ -105,15 +182,21 @@ private MultiMap makeHeaders(Device device, Site site) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); } - if (site != null) { + if (site != null && StringUtils.isNotBlank(site.getPage())) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + try { + final URL url = new URL(site.getPage()); + final String origin = url.getProtocol() + "://" + url.getHost(); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ORIGIN_HEADER, origin); + } catch (MalformedURLException e) { + // do nothing + } } - return headers; } - private String makeUrl(String apiKey) { - return endpointUrl + "?t=%s".formatted(apiKey); + private String resolveEndpointUrl(String apiKey) { + return endpointUrl.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(apiKey)); } private static boolean isGdpr(Regs regs) { @@ -128,7 +211,8 @@ private static String getUserConsent(User user) { return Optional.ofNullable(user) .map(User::getExt) .map(ExtUser::getConsent) - .orElse(StringUtils.EMPTY); + .filter(StringUtils::isNotBlank) + .orElse(null); } @Override diff --git a/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java b/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java new file mode 100644 index 00000000000..e63a704a60c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; // Changed import +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class MissenaUserParams { + + List formats; + + String placement; + + @JsonProperty("test") + String testMode; + + ObjectNode settings; +} + diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java index 016e6ca99d5..0c3a8fd08ab 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java @@ -1,16 +1,25 @@ package org.prebid.server.proto.openrtb.ext.request.missena; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; import lombok.Value; -@Value(staticConstructor = "of") +import java.util.List; + +@Value +@Builder(toBuilder = true) public class ExtImpMissena { @JsonProperty("apiKey") String apiKey; + List formats; + String placement; @JsonProperty("test") String testMode; + + ObjectNode settings; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java index 1c9c7fd355f..798f57dc35e 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java @@ -2,11 +2,13 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.missena.MissenaBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.prebid.server.version.PrebidVersionProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -30,12 +32,15 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps missenaBidderDeps(BidderConfigurationProperties missenaConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + PrebidVersionProvider prebidVersionProvider, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(missenaConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new MissenaBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new MissenaBidder( + config.getEndpoint(), mapper, currencyConversionService, prebidVersionProvider)) .assemble(); } } diff --git a/src/main/resources/bidder-config/missena.yaml b/src/main/resources/bidder-config/missena.yaml index 4f1ea9e4f8f..f8921eac3db 100644 --- a/src/main/resources/bidder-config/missena.yaml +++ b/src/main/resources/bidder-config/missena.yaml @@ -1,6 +1,6 @@ adapters: missena: - endpoint: https://bid.missena.io/ + endpoint: https://bid.missena.io/?t={{PublisherID}} meta-info: maintainer-email: prebid@missena.com modifying-vast-xml-allowed: true diff --git a/src/main/resources/static/bidder-params/missena.json b/src/main/resources/static/bidder-params/missena.json index 86bf5b45dec..be5217efdb1 100644 --- a/src/main/resources/static/bidder-params/missena.json +++ b/src/main/resources/static/bidder-params/missena.json @@ -16,6 +16,17 @@ "test": { "type": "string", "description": "Test Mode" + }, + "formats": { + "type": "array", + "description": "An array of formats to request (banner, native, or video)", + "items": { + "type": "string" + } + }, + "settings": { + "type": "object", + "description": "An object containing extra settings for the Missena adapter" } }, "required": [ diff --git a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java index 83326e4049f..453fcd26739 100644 --- a/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/missena/MissenaBidderTest.java @@ -7,9 +7,15 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -17,20 +23,28 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.missena.ExtImpMissena; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.function.UnaryOperator; -import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; @@ -39,17 +53,37 @@ import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; +@ExtendWith(MockitoExtension.class) class MissenaBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test-url.com"; + private static final String ENDPOINT_URL = "https://test-url.com/?t={{PublisherID}}"; + private static final String TEST_PBS_VERSION = "pbs-java/1.0"; - private final MissenaBidder target = new MissenaBidder(ENDPOINT_URL, jacksonMapper); + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyConversionService; + + @Mock(strictness = LENIENT) + private PrebidVersionProvider prebidVersionProvider; + + private MissenaBidder target; + + @BeforeEach + public void setUp() { + target = new MissenaBidder( + ENDPOINT_URL, + jacksonMapper, + currencyConversionService, + prebidVersionProvider); + + given(prebidVersionProvider.getNameVersionRecord()).willReturn(TEST_PBS_VERSION); + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willAnswer(invocation -> invocation.getArgument(0)); + } @Test public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { // given - final BidRequest bidRequest = givenBidRequest( - imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenInvalidImpExt())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -61,59 +95,90 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { } @Test - public void makeHttpRequestsShouldMakeOneRequestPerImp() { + public void makeHttpRequestsShouldMakeRequestForFirstValidImp() { // given - final BidRequest bidRequest = givenBidRequest( - imp -> imp.id("givenImp1").ext(givenImpExt("apiKey1", "plId1", "test1")), - imp -> imp.id("givenImp2").ext(givenImpExt("apiKey2", "plId2", "test2"))) - .toBuilder() + final ObjectNode settingsNode = mapper.createObjectNode().put("settingKey", "settingValue"); + + final BidRequest bidRequest = BidRequest.builder() .id("requestId") - .site(Site.builder().page("page").domain("domain").build()) + .tmax(500L) + .cur(singletonList("USD")) + .imp(List.of( + givenImp(imp -> imp.id("impId1") + .ext(givenImpExt("apiKey1", "placementId1", "1", List.of("banner"), settingsNode))), + givenImp(imp -> imp.id("impId2") + .ext(givenImpExt("apiKey2", "placementId2", "0", null, null))))) + .site(Site.builder().page("http://test.com/page").domain("test.com").build()) .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) - .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) + .user(User.builder().buyeruid("buyer1") + .ext(ExtUser.builder().consent("consentStr").build()).build()) + .source(Source.builder().schain(SupplyChain.of(1, null, null, null)).build()) + .device(Device.builder().ua("test-ua").ip("123.123.123.123").build()) .build(); - //when + // when final Result>> result = target.makeHttpRequests(bidRequest); - //then - final MissenaAdRequest expectedRequest = MissenaAdRequest.builder() + // then + final MissenaUserParams expectedUserParams = MissenaUserParams.builder() + .formats(List.of("banner")) + .placement("placementId1") + .testMode("1") + .settings(settingsNode) + .build(); + + final MissenaAdRequest expectedPayload = MissenaAdRequest.builder() + .adUnit("impId1") + .buyerUid("buyer1") + .coppa(null) + .currency("USD") + .userEids(null) + .floor(BigDecimal.valueOf(0.1)) + .floorCurrency("USD") + .gdpr(true) + .gdprConsent("consentStr") + .idempotencyKey("requestId") + .referer("http://test.com/page") + .refererCanonical("test.com") .requestId("requestId") - .timeout(2000) - .referer("page") - .refererCanonical("domain") - .consentString("consent") - .consentRequired(true) + .schain(SupplyChain.of(1, null, null, null)) + .timeout(500L) + .params(expectedUserParams) + .version(TEST_PBS_VERSION) .build(); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(2) + assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getPayload) - .containsExactlyInAnyOrder( - expectedRequest.toBuilder().placement("plId1").test("test1").build(), - expectedRequest.toBuilder().placement("plId2").test("test2").build()); + .containsExactly(expectedPayload); + assertThat(result.getValue()) + .extracting(HttpRequest::getImpIds) + .containsExactly(Collections.singleton("impId1")); } @Test - public void makeHttpRequestsShouldHaveImpIds() { + public void makeHttpRequestsShouldReturnErrorIfAllImpsAreInvalid() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp1"), imp -> imp.id("givenImp2")); - //when + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenInvalidImpExt()), + imp -> imp.ext(givenInvalidImpExt())); + + // when final Result>> result = target.makeHttpRequests(bidRequest); - //then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(2) - .extracting(HttpRequest::getImpIds) - .containsExactlyInAnyOrder(singleton("givenImp1"), singleton("givenImp2")); + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(2) + .extracting(BidderError::getMessage) + .containsOnly("Error parsing missenaExt parameters"); } @Test - public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIp() { + public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpAndIpv6() { // given final BidRequest bidRequest = givenBidRequest(identity()) .toBuilder() - .site(Site.builder().page("page").build()) + .site(Site.builder().page("http://page.com").build()) .device(Device.builder().ua("ua").ip("ip").ipv6("ipv6").build()) .build(); @@ -121,19 +186,17 @@ public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIp() { final Result>> result = target.makeHttpRequests(bidRequest); // then + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1).first() .extracting(HttpRequest::getHeaders) - .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) - .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) - .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) - .isEqualTo(APPLICATION_JSON_VALUE)) - .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)) - .isEqualTo("ua")) - .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)) - .isEqualTo("ip")) - .satisfies(headers -> assertThat(headers.get(REFERER_HEADER)) - .isEqualTo("page")); - assertThat(result.getErrors()).isEmpty(); + .satisfies(headers -> { + assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); + assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); + assertThat(headers.get(USER_AGENT_HEADER)).isEqualTo("ua"); + assertThat(headers.getAll(X_FORWARDED_FOR_HEADER)).containsExactlyInAnyOrder("ip", "ipv6"); + assertThat(headers.get(REFERER_HEADER)).isEqualTo("http://page.com"); + assertThat(headers.get(HttpUtil.ORIGIN_HEADER)).isEqualTo("http://page.com"); + }); } @Test @@ -148,45 +211,47 @@ public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceHasIpv6Only() { final Result>> result = target.makeHttpRequests(bidRequest); // then + assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1).first() .extracting(HttpRequest::getHeaders) - .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) - .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) - .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE)) - .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)).isNull()) - .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)).isEqualTo("ipv6")) - .satisfies(headers -> assertThat(headers.get(REFERER_HEADER)).isNull()); - assertThat(result.getErrors()).isEmpty(); + .satisfies(headers -> { + assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); + assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); + assertThat(headers.get(USER_AGENT_HEADER)).isNull(); + assertThat(headers.getAll(X_FORWARDED_FOR_HEADER)).containsExactly("ipv6"); + assertThat(headers.get(REFERER_HEADER)).isNull(); + }); } @Test - public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() { + public void makeHttpRequestsShouldConvertBidFloorCurrency() { // given - final BidRequest bidRequest = givenBidRequest( - imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))), - imp -> imp.id("impId2").ext(givenImpExt("apiKey", "placement", "testMode"))); + final Imp imp = givenImp(i -> i.bidfloor(BigDecimal.TEN).bidfloorcur("EUR") + .ext(givenImpExt("key1"))); + final BidRequest bidRequest = BidRequest.builder().id("reqId").tmax(1000L) + .imp(singletonList(imp)) + .cur(singletonList("USD")) + .build(); + + // Specific mock for this test, overrides the general one in setUp + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "EUR", "USD")) + .willReturn(BigDecimal.valueOf(12)); - //when + // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final MissenaAdRequest expectedRequest = MissenaAdRequest.builder() - .timeout(2000) - .consentString("") - .consentRequired(false) - .test("testMode") - .placement("placement") - .build(); - - assertThat(result.getValue()).hasSize(1) - .extracting(HttpRequest::getPayload) - .containsOnly(expectedRequest); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().getFirst().getPayload()) + .extracting(MissenaAdRequest::getFloor, MissenaAdRequest::getFloorCurrency) + .containsExactly(BigDecimal.valueOf(12), "USD"); } @Test public void makeHttpRequestsShouldUseCorrectUri() { // given - final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("apiKey", "plId", "test"))); + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("testApiKey"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -195,15 +260,14 @@ public void makeHttpRequestsShouldUseCorrectUri() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) .extracting(HttpRequest::getUri) - .containsExactly("https://test-url.com?t=apiKey"); + .containsExactly("https://test-url.com/?t=testApiKey"); } @Test public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // given final BidderCall httpCall = givenHttpCall("invalid"); - final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1"), imp -> imp.id("impId2")) - .toBuilder().id("requestId").build(); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1")); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -225,9 +289,9 @@ public void makeBidsShouldReturnSingleBid() throws JsonProcessingException { .currency("USD") .ad("adm") .build(); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(bidResponse)); - final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId1"), imp -> imp.id("impId2")) - .toBuilder().id("requestId").build(); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId")).toBuilder().id("requestId").build(); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -235,33 +299,57 @@ public void makeBidsShouldReturnSingleBid() throws JsonProcessingException { // then assertThat(result.getErrors()).isEmpty(); - final Bid expetedBid = Bid.builder() - .adm("adm") + final Bid expectedBid = Bid.builder() + .id("requestId") + .impid("impId") .price(BigDecimal.TEN) + .adm("adm") .crid("id") - .impid("impId1") - .id("requestId") .build(); assertThat(result.getValue()).hasSize(1) - .containsOnly(BidderBid.of(expetedBid, BidType.banner, "USD")); + .containsOnly(BidderBid.of(expectedBid, BidType.banner, "USD")); } private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { - return BidRequest.builder() - .imp(Arrays.stream(impCustomizers).map(MissenaBidderTest::givenImp).toList()) - .build(); + final List imps = Arrays.stream(impCustomizers) + .map(MissenaBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).cur(singletonList("USD")).build(); } private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id("impId") - .ext(givenImpExt("apikey", "placementId", "test"))) + .bidfloor(BigDecimal.valueOf(0.1)) + .bidfloorcur("USD") + .ext(givenImpExt("defaultApiKey"))) + .build(); + } + + private static ObjectNode givenImpExt(String apiKey) { + return givenImpExt(apiKey, null, null, null, null); + } + + private static ObjectNode givenImpExt(String apiKey, + String placement, + String testMode, + List formats, + ObjectNode settings) { + + final ExtImpMissena extImpMissena = ExtImpMissena.builder() + .apiKey(apiKey) + .placement(placement) + .testMode(testMode) + .formats(formats) + .settings(settings) .build(); + + return mapper.valueToTree(ExtPrebid.of(null, extImpMissena)); } - private static ObjectNode givenImpExt(String apiKey, String placement, String testMode) { - return mapper.valueToTree(ExtPrebid.of(null, ExtImpMissena.of(apiKey, placement, testMode))); + private static ObjectNode givenInvalidImpExt() { + return mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())); } private static BidderCall givenHttpCall(String body) { @@ -270,5 +358,4 @@ private static BidderCall givenHttpCall(String body) { HttpResponse.of(200, null, body), null); } - } diff --git a/src/test/java/org/prebid/server/it/MissenaTest.java b/src/test/java/org/prebid/server/it/MissenaTest.java index e86227fb358..abb37647910 100644 --- a/src/test/java/org/prebid/server/it/MissenaTest.java +++ b/src/test/java/org/prebid/server/it/MissenaTest.java @@ -8,7 +8,6 @@ import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; @@ -20,7 +19,6 @@ public class MissenaTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromMissena() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/missena-exchange")) - .withQueryParam("t", equalTo("apiKey")) .withRequestBody(equalToJson(jsonFrom("openrtb2/missena/test-missena-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/missena/test-missena-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json index aa398c49b22..5d6eacd1e38 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-missena-bid-request.json @@ -1,10 +1,15 @@ { - "request_id": "request_id", - "timeout": 2000, - "referer": "http://www.example.com", - "referer_canonical": "www.example.com", - "consent_string": "", - "consent_required": false, - "placement": "placement", - "test": "test" + "adunit" : "imp_id", + "currency" : "USD", + "consent_required" : false, + "ik" : "request_id", + "referer" : "http://www.example.com", + "referer_canonical" : "www.example.com", + "request_id" : "request_id", + "timeout" : "${json-unit.any-number}", + "params" : { + "placement" : "placement", + "test" : "test" + }, + "version" : "${json-unit.any-string}" } From 6c495aea6284c96cd4193fa6a5c225b2db9d8a17 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:46:18 +0200 Subject: [PATCH 19/51] Adnuntius Adapter: Add multi-format and native support (#3964) --- .../bidder/adnuntius/AdnuntiusBidder.java | 209 +++-- .../model/request/AdnuntiusNativeRequest.java | 11 + .../model/request/AdnuntiusRequest.java | 2 +- ...dUnit.java => AdnuntiusRequestAdUnit.java} | 8 +- ...ntiusAdsUnit.java => AdnuntiusAdUnit.java} | 9 +- .../model/response/AdnuntiusResponse.java | 2 +- .../bidder/AdnuntiusBidderConfiguration.java | 25 +- .../resources/bidder-config/adnuntius.yaml | 3 + .../bidder/adnuntius/AdnuntiusBidderTest.java | 788 +++++++++++++++--- .../org/prebid/server/it/AdnuntiusTest.java | 2 +- .../adnuntius/test-adnuntius-bid-request.json | 2 +- .../test-adnuntius-bid-response.json | 3 +- .../test-auction-adnuntius-response.json | 1 + .../server/it/test-application.properties | 1 + 14 files changed, 866 insertions(+), 200 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusNativeRequest.java rename src/main/java/org/prebid/server/bidder/adnuntius/model/request/{AdnuntiusAdUnit.java => AdnuntiusRequestAdUnit.java} (71%) rename src/main/java/org/prebid/server/bidder/adnuntius/model/response/{AdnuntiusAdsUnit.java => AdnuntiusAdUnit.java} (63%) diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java index 52ec2b3992a..1603cdc855b 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.adnuntius; +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.ObjectNode; @@ -23,11 +24,12 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusAdUnit; import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusMetaData; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusNativeRequest; import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequestAdUnit; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; -import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdUnit; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBidExt; @@ -41,6 +43,7 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; +import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; @@ -51,6 +54,7 @@ import org.prebid.server.proto.openrtb.ext.request.adnuntius.ExtImpAdnuntius; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; @@ -65,8 +69,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; public class AdnuntiusBidder implements Bidder { @@ -74,48 +76,62 @@ public class AdnuntiusBidder implements Bidder { new TypeReference<>() { }; private static final int SECONDS_IN_MINUTE = 60; - private static final String TARGET_ID_DELIMITER = "-"; private static final String DEFAULT_PAGE = "unknown"; private static final String DEFAULT_NETWORK = "default"; private static final BigDecimal PRICE_MULTIPLIER = BigDecimal.valueOf(1000); + private static final int BANNER_MTYPE = 1; + private static final int NATIVE_MTYPE = 4; private final String endpointUrl; + private final String euEndpoint; private final Clock clock; private final JacksonMapper mapper; - public AdnuntiusBidder(String endpointUrl, Clock clock, JacksonMapper mapper) { + public AdnuntiusBidder(String endpointUrl, + String euEndpoint, + Clock clock, + JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.euEndpoint = euEndpoint == null ? null : HttpUtil.validateUrl(euEndpoint); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { - final Map> networkToAdUnits = new HashMap<>(); - boolean noCookies = false; - - for (Imp imp : request.getImp()) { - final ExtImpAdnuntius extImpAdnuntius; - try { + try { + final Map> networkToAdUnits = new HashMap<>(); + boolean noCookies = false; + for (Imp imp : request.getImp()) { validateImp(imp); - extImpAdnuntius = parseImpExt(imp); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } + final ExtImpAdnuntius extImpAdnuntius = parseImpExt(imp); + noCookies = noCookies || resolveIsNoCookies(extImpAdnuntius); + final String network = resolveNetwork(extImpAdnuntius); - noCookies = noCookies || resolveIsNoCookies(extImpAdnuntius); - final String network = resolveNetwork(extImpAdnuntius); + final List adUnits = networkToAdUnits.computeIfAbsent( + network, + ignored -> new ArrayList<>()); - networkToAdUnits.computeIfAbsent(network, ignored -> new ArrayList<>()) - .add(makeAdUnit(imp, extImpAdnuntius)); - } + if (imp.getBanner() != null) { + adUnits.add(makeBannerAdUnit(imp, extImpAdnuntius)); + } - return Result.withValues(createHttpRequests(networkToAdUnits, request, noCookies)); + if (imp.getXNative() != null) { + adUnits.add(makeNativeAdUnit(imp, extImpAdnuntius)); + } + } + + return Result.withValues(createHttpRequests(networkToAdUnits, request, noCookies)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } } private static void validateImp(Imp imp) { - if (imp.getBanner() == null) { - throw new PreBidException("Fail on Imp.Id=%s: Adnuntius supports only Banner".formatted(imp.getId())); + if (imp.getBanner() == null && imp.getXNative() == null) { + throw new PreBidException("ignoring imp id=%s: Adnuntius supports only native and banner" + .formatted(imp.getId())); } } @@ -141,18 +157,18 @@ private static String resolveNetwork(ExtImpAdnuntius extImpAdnuntius) { .orElse(DEFAULT_NETWORK); } - private static AdnuntiusAdUnit makeAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { + private static AdnuntiusRequestAdUnit makeBannerAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { final String auId = extImpAdnuntius.getAuId(); - return AdnuntiusAdUnit.builder() + return AdnuntiusRequestAdUnit.builder() .auId(auId) - .targetId(targetId(auId, imp.getId())) + .targetId(targetId(auId, imp.getId(), "banner")) .dimensions(createDimensions(imp.getBanner())) .maxDeals(resolveMaxDeals(extImpAdnuntius)) .build(); } - private static String targetId(String auId, String impId) { - return auId + TARGET_ID_DELIMITER + impId; + private static String targetId(String auId, String impId, String bidType) { + return "%s-%s:%s".formatted(auId, impId, bidType); } private static List> createDimensions(Banner banner) { @@ -166,6 +182,7 @@ private static List> createDimensions(Banner banner) { formats.add(List.of(w, h)); } } + if (!formats.isEmpty()) { return formats; } @@ -179,44 +196,70 @@ private static List> createDimensions(Banner banner) { return formats.isEmpty() ? null : formats; } + private AdnuntiusRequestAdUnit makeNativeAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { + final String auId = extImpAdnuntius.getAuId(); + return AdnuntiusRequestAdUnit.builder() + .auId(auId) + .adType("NATIVE") + .targetId(targetId(auId, imp.getId(), "native")) + .maxDeals(resolveMaxDeals(extImpAdnuntius)) + .nativeRequest(AdnuntiusNativeRequest.of(parseNativeRequest(imp))) + .build(); + } + + private ObjectNode parseNativeRequest(Imp imp) { + try { + return mapper.mapper().readValue(imp.getXNative().getRequest(), ObjectNode.class); + } catch (IllegalArgumentException | JsonProcessingException e) { + throw new PreBidException("Unmarshalling Native error " + e.getMessage()); + } + } + private static Integer resolveMaxDeals(ExtImpAdnuntius extImpAdnuntius) { final Integer maxDeals = extImpAdnuntius.getMaxDeals(); return maxDeals != null && maxDeals > 0 ? maxDeals : null; } - private List> createHttpRequests(Map> networkToAdUnits, - BidRequest request, - boolean noCookies) { + private List> createHttpRequests( + Map> networkToAdUnits, + BidRequest request, + boolean noCookies) { final Site site = request.getSite(); - final String uri = createUri(request, noCookies); + final String uri = makeEndpoint(request, noCookies); final String page = extractPage(site); final ObjectNode data = extractData(site); final AdnuntiusMetaData metaData = createMetaData(request.getUser()); final List> adnuntiusRequests = new ArrayList<>(); - for (List adUnits : networkToAdUnits.values()) { + for (List adUnits : networkToAdUnits.values()) { final AdnuntiusRequest adnuntiusRequest = AdnuntiusRequest.builder() .adUnits(adUnits) .context(page) .keyValue(data) .metaData(metaData) .build(); - adnuntiusRequests.add(createHttpRequest(adnuntiusRequest, uri, request.getDevice())); + adnuntiusRequests.add(createHttpRequest(request, adnuntiusRequest, uri, request.getDevice())); } return adnuntiusRequests; } - private String createUri(BidRequest bidRequest, Boolean noCookies) { + private String makeEndpoint(BidRequest bidRequest, Boolean noCookies) { try { - final URIBuilder uriBuilder = new URIBuilder(endpointUrl) + final String gdpr = extractGdpr(bidRequest.getRegs()); + final String url = StringUtils.isNotBlank(gdpr) ? euEndpoint : endpointUrl; + + if (url == null) { + throw new PreBidException("an EU endpoint is required but invalid"); + } + + final URIBuilder uriBuilder = new URIBuilder(url) .addParameter("format", "prebidServer") .addParameter("tzo", getTimeZoneOffset()); - final String gdpr = extractGdpr(bidRequest.getRegs()); if (StringUtils.isNotEmpty(gdpr)) { uriBuilder.addParameter("gdpr", gdpr); } @@ -231,7 +274,7 @@ private String createUri(BidRequest bidRequest, Boolean noCookies) { } return uriBuilder.build().toString(); - } catch (URISyntaxException e) { + } catch (URISyntaxException | IllegalArgumentException e) { throw new PreBidException(e.getMessage()); } } @@ -297,7 +340,8 @@ private static AdnuntiusMetaData createMetaData(User user) { .orElse(null); } - private HttpRequest createHttpRequest(AdnuntiusRequest adnuntiusRequest, + private HttpRequest createHttpRequest(BidRequest request, + AdnuntiusRequest adnuntiusRequest, String uri, Device device) { @@ -307,6 +351,7 @@ private HttpRequest createHttpRequest(AdnuntiusRequest adnunti .uri(uri) .body(mapper.encodeToBytes(adnuntiusRequest)) .payload(adnuntiusRequest) + .impIds(BidderUtil.impIds(request)) .build(); } @@ -327,59 +372,102 @@ public Result> makeBids(BidderCall httpCall, B final String body = httpCall.getResponse().getBody(); final AdnuntiusResponse adnuntiusResponse = mapper.decodeValue(body, AdnuntiusResponse.class); return Result.withValues(extractBids(bidRequest, adnuntiusResponse)); - } catch (DecodeException | PreBidException e) { + } catch (EncodeException | DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } + private static Map parseAdUnits(AdnuntiusResponse adnuntiusResponse) { + final Map targetIdToAdsUnit = new HashMap<>(); + for (AdnuntiusAdUnit adUnit : adnuntiusResponse.getAdUnits()) { + if (isValid(adUnit)) { + final String targetId = extractTargetId(adUnit.getTargetId()); + final AdnuntiusAdUnit existingAdUnit = targetIdToAdsUnit.get(targetId); + if (existingAdUnit == null || getBidAmount(adUnit).compareTo(getBidAmount(existingAdUnit)) >= 0) { + targetIdToAdsUnit.put(targetId, adUnit); + } + } + } + + return targetIdToAdsUnit; + } + private List extractBids(BidRequest bidRequest, AdnuntiusResponse adnuntiusResponse) { - if (adnuntiusResponse == null || CollectionUtils.isEmpty(adnuntiusResponse.getAdsUnits())) { + if (adnuntiusResponse == null || CollectionUtils.isEmpty(adnuntiusResponse.getAdUnits())) { return Collections.emptyList(); } - final Map targetIdToAdsUnit = adnuntiusResponse.getAdsUnits().stream() - .filter(AdnuntiusBidder::validateAdsUnit) - .collect(Collectors.toMap( - AdnuntiusAdsUnit::getTargetId, - Function.identity(), - (first, second) -> second)); + final Map targetIdToAdsUnit = parseAdUnits(adnuntiusResponse); String currency = null; final List bids = new ArrayList<>(); for (Imp imp : bidRequest.getImp()) { final ExtImpAdnuntius extImpAdnuntius = parseImpExt(imp); - final String targetId = targetId(extImpAdnuntius.getAuId(), imp.getId()); + final String targetId = targetIdForBids(extImpAdnuntius.getAuId(), imp.getId()); - final AdnuntiusAdsUnit adsUnit = targetIdToAdsUnit.get(targetId); - if (adsUnit == null) { + final AdnuntiusAdUnit adUnit = targetIdToAdsUnit.get(targetId); + if (adUnit == null) { continue; } - final AdnuntiusAd ad = adsUnit.getAds().getFirst(); + final AdnuntiusAd ad = adUnit.getAds().getFirst(); final String impId = imp.getId(); final String bidType = extImpAdnuntius.getBidType(); + currency = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getCurrency); + final JsonNode nativeRequest = Optional.ofNullable(adUnit.getNativeJson()) + .map(AdnuntiusNativeRequest::getOrtb) + .orElse(null); + final int mType = nativeRequest == null ? BANNER_MTYPE : NATIVE_MTYPE; + final String html = nativeRequest == null ? adUnit.getHtml() : mapper.encodeToString(nativeRequest); - bids.add(createBid(ad, bidRequest, adsUnit.getHtml(), impId, bidType)); + bids.add(createBid(ad, bidRequest, html, impId, bidType, mType)); - for (AdnuntiusAd deal : ListUtils.emptyIfNull(adsUnit.getDeals())) { - bids.add(createBid(deal, bidRequest, deal.getHtml(), impId, bidType)); + for (AdnuntiusAd deal : ListUtils.emptyIfNull(adUnit.getDeals())) { + bids.add(createBid(deal, bidRequest, deal.getHtml(), impId, bidType, BANNER_MTYPE)); } } final String lastCurrency = currency; return bids.stream() - .map(bid -> BidderBid.of(bid, BidType.banner, lastCurrency)) + .map(bid -> BidderBid.of( + bid, + bid.getMtype() == BANNER_MTYPE ? BidType.banner : BidType.xNative, + lastCurrency)) .toList(); } - private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { - final List ads = adsUnit != null ? adsUnit.getAds() : null; - return CollectionUtils.isNotEmpty(ads) && ads.getFirst() != null; + private static BigDecimal getBidAmount(AdnuntiusAdUnit adUnit) { + return adUnit.getAds().getFirst().getBid().getAmount(); + } + + private static boolean isValid(AdnuntiusAdUnit adsUnit) { + if (adsUnit == null) { + return false; + } + + final String targetId = extractTargetId(adsUnit.getTargetId()); + final int matchedCount = ObjectUtils.defaultIfNull(adsUnit.getMatchedAdCount(), 0); + final List ads = adsUnit.getAds(); + final BigDecimal bidAmount = CollectionUtils.emptyIfNull(ads).stream() + .findFirst() + .map(AdnuntiusAd::getBid) + .map(AdnuntiusBid::getAmount) + .orElse(null); + + return targetId != null && matchedCount > 0 && bidAmount != null; + } + + private static String extractTargetId(String targetId) { + return targetId == null ? null : targetId.split(":")[0]; + } + + private static String targetIdForBids(String auId, String impId) { + return "%s-%s".formatted(auId, impId); } - private Bid createBid(AdnuntiusAd ad, BidRequest bidRequest, String adm, String impId, String bidType) { + private Bid createBid(AdnuntiusAd ad, BidRequest bidRequest, String adm, String impId, String bidType, int mtype) { final String adId = ad.getAdId(); final AdnuntiusBidExt bidExt = prepareBidExt(ad, bidRequest); @@ -395,6 +483,7 @@ private Bid createBid(AdnuntiusAd ad, BidRequest bidRequest, String adm, String .price(resolvePrice(ad, bidType)) .adm(adm) .adomain(ad.getAdvertiserDomains()) + .mtype(mtype) .ext(bidExt == null ? null : mapper.mapper().valueToTree(bidExt)) .build(); } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusNativeRequest.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusNativeRequest.java new file mode 100644 index 00000000000..b76f5394b4d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusNativeRequest.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.adnuntius.model.request; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AdnuntiusNativeRequest { + + ObjectNode ortb; + +} diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java index eafd24facbf..2fde60d0044 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequest.java @@ -13,7 +13,7 @@ public class AdnuntiusRequest { @JsonProperty("adUnits") - List adUnits; + List adUnits; @JsonProperty("metaData") @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusAdUnit.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequestAdUnit.java similarity index 71% rename from src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusAdUnit.java rename to src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequestAdUnit.java index f092822f909..03e9b586c72 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusAdUnit.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequestAdUnit.java @@ -8,7 +8,7 @@ @Builder(toBuilder = true) @Value -public class AdnuntiusAdUnit { +public class AdnuntiusRequestAdUnit { @JsonProperty("auId") String auId; @@ -20,4 +20,10 @@ public class AdnuntiusAdUnit { @JsonProperty("maxDeals") Integer maxDeals; + + @JsonProperty("nativeRequest") + AdnuntiusNativeRequest nativeRequest; + + @JsonProperty("adType") + String adType; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdsUnit.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdUnit.java similarity index 63% rename from src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdsUnit.java rename to src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdUnit.java index 8fb0c0c7c70..7dbd6478335 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdsUnit.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdUnit.java @@ -3,12 +3,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusNativeRequest; import java.util.List; @Builder @Value -public class AdnuntiusAdsUnit { +public class AdnuntiusAdUnit { @JsonProperty("auId") String auId; @@ -18,9 +19,15 @@ public class AdnuntiusAdsUnit { String html; + @JsonProperty("matchedAdCount") + Integer matchedAdCount; + @JsonProperty("responseId") String responseId; + @JsonProperty("nativeJson") + AdnuntiusNativeRequest nativeJson; + List ads; List deals; diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java index 6ce62d9afdb..ca4a6d5fe5f 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java @@ -9,5 +9,5 @@ public class AdnuntiusResponse { @JsonProperty("adUnits") - List adsUnits; + List adUnits; } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java index 687c2897b8f..3aca2e59cf8 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdnuntiusBidderConfiguration.java @@ -1,5 +1,8 @@ package org.prebid.server.spring.config.bidder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.adnuntius.AdnuntiusBidder; import org.prebid.server.json.JacksonMapper; @@ -24,20 +27,32 @@ public class AdnuntiusBidderConfiguration { @Bean("adnuntiusConfigurationProperties") @ConfigurationProperties("adapters.adnuntius") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); + AdnuntiusConfigurationProperties configurationProperties() { + return new AdnuntiusConfigurationProperties(); } @Bean - BidderDeps adnuntiusBidderDeps(BidderConfigurationProperties adnuntiusConfigurationProperties, + BidderDeps adnuntiusBidderDeps(AdnuntiusConfigurationProperties adnuntiusConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, Clock clock, JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) + return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(adnuntiusConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new AdnuntiusBidder(config.getEndpoint(), clock, mapper)) + .bidderCreator(config -> new AdnuntiusBidder( + config.getEndpoint(), + config.getEuEndpoint(), + clock, + mapper)) .assemble(); } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class AdnuntiusConfigurationProperties extends BidderConfigurationProperties { + + private String euEndpoint; + } } diff --git a/src/main/resources/bidder-config/adnuntius.yaml b/src/main/resources/bidder-config/adnuntius.yaml index b90b743f81d..b8a3006ae24 100644 --- a/src/main/resources/bidder-config/adnuntius.yaml +++ b/src/main/resources/bidder-config/adnuntius.yaml @@ -1,11 +1,14 @@ adapters: adnuntius: endpoint: https://ads.adnuntius.delivery/i + eu-endpoint: https://europe.delivery.adnuntius.com/i meta-info: maintainer-email: hello@adnuntius.com app-media-types: - banner + - native site-media-types: - banner + - native supported-vendors: vendor-id: 855 diff --git a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java index afcc617ebfa..910a2b6514c 100644 --- a/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidderTest.java @@ -10,6 +10,7 @@ import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Uid; @@ -20,11 +21,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; -import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusAdUnit; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusNativeRequest; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequestAdUnit; import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusMetaData; import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAd; -import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdsUnit; +import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdUnit; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusAdvertiser; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusBid; import org.prebid.server.bidder.adnuntius.model.response.AdnuntiusGrossBid; @@ -51,6 +53,7 @@ import java.time.ZoneId; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; @@ -61,24 +64,46 @@ public class AdnuntiusBidderTest extends VertxTest { + private static final String ENDPOINT_URL = "https://test.domain.dm/uri"; + private static final String ALTERNATIVE_URL = "https://alternative.domain.dm/uri"; + private AdnuntiusBidder target; @BeforeEach public void setUp() { final Clock clock = Clock.system(ZoneId.of("UTC+05:00")); - target = new AdnuntiusBidder("https://test.domain.dm/uri", clock, jacksonMapper); + target = new AdnuntiusBidder( + ENDPOINT_URL, + null, + clock, + jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> - new AdnuntiusBidder("invalid_url", Clock.systemDefaultZone(), jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> new AdnuntiusBidder( + "invalid_url", + null, + Clock.systemDefaultZone(), + jacksonMapper)); + } + + @Test + public void creationShouldFailOnInvalidEuEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AdnuntiusBidder( + ENDPOINT_URL, + "invalid_url", + Clock.systemDefaultZone(), + jacksonMapper)); } @Test - public void makeHttpRequestsShouldReturnErrorWhenSomeImpBannerIsAbsent() { + public void makeHttpRequestsShouldReturnErrorWhenSomeImpBannerAndNativeIsAbsent() { // given - final BidRequest bidRequest = givenBidRequest(givenImp(identity()), givenImp(imp -> imp.banner(null))); + final BidRequest bidRequest = givenBidRequest( + givenBannerImp(identity()), + givenNativeImp(identity()), + givenBannerImp(imp -> imp.banner(null).xNative(null))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -86,14 +111,14 @@ public void makeHttpRequestsShouldReturnErrorWhenSomeImpBannerIsAbsent() { // then assertThat(result.getValue()).isEmpty(); assertThat(result.getErrors()).extracting(BidderError::getMessage) - .containsExactly("Fail on Imp.Id=impId: Adnuntius supports only Banner"); + .containsExactly("ignoring imp id=impId: Adnuntius supports only native and banner"); } @Test public void makeHttpRequestsShouldReturnErrorWhenSomeImpExtCouldNotBeParsed() { // given - final BidRequest bidRequest = givenBidRequest(givenImp(identity()), - givenImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))); + final BidRequest bidRequest = givenBidRequest(givenBannerImp(identity()), + givenBannerImp(imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -108,7 +133,8 @@ public void makeHttpRequestsShouldReturnErrorWhenSomeImpExtCouldNotBeParsed() { public void makeHttpRequestsShouldReturnRequestsWithMaxDealsIfMaxDealsIsBiggestThatZero() { // given final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().maxDeals(10).build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().maxDeals(10).build(), identity()), + givenNativeImp(ExtImpAdnuntius.builder().maxDeals(5).build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -118,15 +144,16 @@ public void makeHttpRequestsShouldReturnRequestsWithMaxDealsIfMaxDealsIsBiggestT assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(AdnuntiusRequest::getAdUnits) - .extracting(AdnuntiusAdUnit::getMaxDeals) - .containsExactly(10); + .extracting(AdnuntiusRequestAdUnit::getMaxDeals) + .containsExactly(10, 5); } @Test public void makeHttpRequestsShouldNotReturnRequestsWithMaxDealsIfMaxDealsIsLowestThatZero() { // given final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().maxDeals(-10).build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().maxDeals(-10).build(), identity()), + givenNativeImp(ExtImpAdnuntius.builder().maxDeals(-10).build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -136,7 +163,41 @@ public void makeHttpRequestsShouldNotReturnRequestsWithMaxDealsIfMaxDealsIsLowes assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(AdnuntiusRequest::getAdUnits) - .extracting(AdnuntiusAdUnit::getMaxDeals) + .extracting(AdnuntiusRequestAdUnit::getMaxDeals) + .containsOnlyNulls(); + } + + @Test + public void makeHttpRequestsShouldReturnAdTypeForNativeImp() { + // given + final BidRequest bidRequest = givenBidRequest(givenNativeImp(identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(AdnuntiusRequest::getAdUnits) + .extracting(AdnuntiusRequestAdUnit::getAdType) + .containsOnly("NATIVE"); + } + + @Test + public void makeHttpRequestsShouldNotReturnAdTypeForBannerImp() { + // given + final BidRequest bidRequest = givenBidRequest(givenBannerImp(identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(AdnuntiusRequest::getAdUnits) + .extracting(AdnuntiusRequestAdUnit::getAdType) .containsNull(); } @@ -144,7 +205,7 @@ public void makeHttpRequestsShouldNotReturnRequestsWithMaxDealsIfMaxDealsIsLowes public void makeHttpRequestsShouldReturnRequestsWithDimensionsIfBannerHighAndWidthArePresent() { // given final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.banner(Banner.builder().w(150).h(200).build()))); + givenBannerImp(imp -> imp.banner(Banner.builder().w(150).h(200).build()))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -154,7 +215,7 @@ public void makeHttpRequestsShouldReturnRequestsWithDimensionsIfBannerHighAndWid assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(AdnuntiusRequest::getAdUnits) - .extracting(AdnuntiusAdUnit::getDimensions) + .extracting(AdnuntiusRequestAdUnit::getDimensions) .containsExactly(List.of(List.of(150, 200))); } @@ -162,7 +223,7 @@ public void makeHttpRequestsShouldReturnRequestsWithDimensionsIfBannerHighAndWid public void makeHttpRequestsShouldReturnRequestsWithDimensionsIfBannerFormatHighAndWidthArePresent() { // given final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.banner(Banner.builder() + givenBannerImp(imp -> imp.banner(Banner.builder() .format(List.of( Format.builder().w(150).h(200).build(), Format.builder().w(100).h(300).build())) @@ -178,14 +239,14 @@ public void makeHttpRequestsShouldReturnRequestsWithDimensionsIfBannerFormatHigh assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(AdnuntiusRequest::getAdUnits) - .extracting(AdnuntiusAdUnit::getDimensions) + .extracting(AdnuntiusRequestAdUnit::getDimensions) .containsExactly(List.of(List.of(150, 200), List.of(100, 300))); } @Test public void makeHttpRequestsShouldReturnRequestsWithoutDimensionsIfBannerFormatHighAndWidthAreAbsent() { // given - final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidRequest bidRequest = givenBidRequest(givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -195,18 +256,37 @@ public void makeHttpRequestsShouldReturnRequestsWithoutDimensionsIfBannerFormatH assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(AdnuntiusRequest::getAdUnits) - .extracting(AdnuntiusAdUnit::getDimensions) + .extracting(AdnuntiusRequestAdUnit::getDimensions) .containsOnlyNulls(); } @Test - public void makeHttpRequestsShouldReturnRequestsWithAdUnitsSeparatedByImpExtNetwork() { + public void makeHttpRequestsShouldReturnNativeRequestAdUnitForNativeImp() { + // given + final BidRequest bidRequest = givenBidRequest(givenNativeImp(imp -> + imp.xNative(Native.builder().request("{\"field\":\"value\"}").build()))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(AdnuntiusRequest::getAdUnits) + .extracting(AdnuntiusRequestAdUnit::getNativeRequest) + .extracting(AdnuntiusNativeRequest::getOrtb) + .allSatisfy(ortb -> assertThat(ortb).isEqualTo(mapper.createObjectNode().put("field", "value"))); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithAdUnitsSeparatedByBannerImpExtNetwork() { // given final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("auId1").build(), identity()), - givenImp(ExtImpAdnuntius.builder().auId("auId2").build(), identity()), - givenImp(ExtImpAdnuntius.builder().auId("auId1").network("network").build(), identity()), - givenImp(ExtImpAdnuntius.builder().auId("auId2").network("network").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("auId1").build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().auId("auId2").build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().auId("auId1").network("network").build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().auId("auId2").network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -216,7 +296,29 @@ public void makeHttpRequestsShouldReturnRequestsWithAdUnitsSeparatedByImpExtNetw .extracting(HttpRequest::getPayload) .extracting(AdnuntiusRequest::getAdUnits) .allSatisfy(adUnits -> assertThat(adUnits) - .extracting(AdnuntiusAdUnit::getAuId) + .extracting(AdnuntiusRequestAdUnit::getAuId) + .containsExactly("auId1", "auId2")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithAdUnitsSeparatedByNativeImpExtNetwork() { + // given + final BidRequest bidRequest = givenBidRequest( + givenNativeImp(ExtImpAdnuntius.builder().auId("auId1").build(), identity()), + givenNativeImp(ExtImpAdnuntius.builder().auId("auId2").build(), identity()), + givenNativeImp(ExtImpAdnuntius.builder().auId("auId1").network("network").build(), identity()), + givenNativeImp(ExtImpAdnuntius.builder().auId("auId2").network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getPayload) + .extracting(AdnuntiusRequest::getAdUnits) + .allSatisfy(adUnits -> assertThat(adUnits) + .extracting(AdnuntiusRequestAdUnit::getAuId) .containsExactly("auId1", "auId2")); assertThat(result.getErrors()).isEmpty(); } @@ -225,9 +327,9 @@ public void makeHttpRequestsShouldReturnRequestsWithAdUnitsSeparatedByImpExtNetw public void makeHttpRequestsShouldReturnRequestsWithCorrectAdUnits() { // given final BidRequest bidRequest = givenBidRequest( - givenImp(imp -> imp.id(null)), - givenImp(ExtImpAdnuntius.builder().auId("auId").build(), imp -> imp.id(null)), - givenImp(ExtImpAdnuntius.builder().auId("auId").build(), imp -> imp.id("impId"))); + givenBannerAndNativeImp(ExtImpAdnuntius.builder().build(), imp -> imp.id(null)), + givenBannerAndNativeImp(ExtImpAdnuntius.builder().auId("auId").build(), imp -> imp.id(null)), + givenBannerAndNativeImp(ExtImpAdnuntius.builder().auId("auId").build(), imp -> imp.id("impId"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -236,8 +338,35 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectAdUnits() { assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getPayload) .flatExtracting(AdnuntiusRequest::getAdUnits) - .extracting(AdnuntiusAdUnit::getAuId, AdnuntiusAdUnit::getTargetId) - .containsExactly(tuple(null, "null-null"), tuple("auId", "auId-null"), tuple("auId", "auId-impId")); + .extracting(AdnuntiusRequestAdUnit::getAuId, AdnuntiusRequestAdUnit::getTargetId) + .containsExactly( + tuple(null, "null-null:banner"), + tuple(null, "null-null:native"), + tuple("auId", "auId-null:banner"), + tuple("auId", "auId-null:native"), + tuple("auId", "auId-impId:banner"), + tuple("auId", "auId-impId:native")); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithTheWholeListOfImpIds() { + // given + final BidRequest bidRequest = givenBidRequest( + givenBannerAndNativeImp( + ExtImpAdnuntius.builder().auId("auId").build(), imp -> imp.id("impId1")), + givenNativeImp( + ExtImpAdnuntius.builder().auId("auId").build(), imp -> imp.id("impId2")), + givenBannerImp( + ExtImpAdnuntius.builder().auId("auId").network("network").build(), imp -> imp.id("impId3"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactlyInAnyOrder(Set.of("impId1", "impId2", "impId3"), Set.of("impId1", "impId2", "impId3")); assertThat(result.getErrors()).isEmpty(); } @@ -246,8 +375,8 @@ public void makeHttpRequestsShouldReturnRequestsWithMetaDataIfUserIdIsPresent() // given final BidRequest bidRequest = givenBidRequest( request -> request.user(User.builder().id("userId").build()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), - givenImp(identity())); + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -271,8 +400,8 @@ public void makeHttpRequestsShouldPopulateMetaDataUsiFromUserIdWhenBothUidIdAndU .eids(List.of(Eid.builder().uids(List.of(Uid.builder().id("eidsId").build())).build())) .build()) .build()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), - givenImp(identity())); + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -296,8 +425,8 @@ public void makeHttpRequestsShouldPopulateMetaDataUsiWhenUserExtEidsUidIdPresent .eids(List.of(Eid.builder().uids(List.of(Uid.builder().id("eidsId").build())).build())) .build()) .build()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), - givenImp(identity())); + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -318,8 +447,8 @@ public void makeHttpRequestsShouldPopulateHttpRequestKeyValueFieldFromSiteExtDat request -> request.site(Site.builder() .ext(ExtSite.of(null, mapper.createObjectNode().put("ANY", "ANY"))) .build()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), - givenImp(identity())); + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -339,7 +468,7 @@ public void makeHttpRequestsShouldReturnRequestsWithHeadersIfDeviceIsPresent() { // given final BidRequest bidRequest = givenBidRequest( request -> request.device(Device.builder().ip("ip").ua("ua").build()), - givenImp(identity())); + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -362,8 +491,8 @@ public void makeHttpRequestsShouldReturnRequestsWithContextIfSitePageIsPresent() // given final BidRequest bidRequest = givenBidRequest( request -> request.site(Site.builder().page("page").build()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), - givenImp(identity())); + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -382,14 +511,14 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprAndConsentAr request -> request .regs(Regs.builder().ext(ExtRegs.of(null, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(null, null); + final String expectedUrl = buildExpectedUrl(ENDPOINT_URL, null, null, null); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -404,14 +533,14 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprIsAbsent() { request -> request .regs(Regs.builder().ext(ExtRegs.of(null, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(null, "consent"); + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, null, "consent"); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -422,18 +551,223 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfGdprIsAbsent() { @Test public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfConsentIsAbsent() { // given + final BidRequest bidRequest = givenBidRequest( + request -> request.user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, null); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesIsNull() { + // given + final BidRequest bidRequest = givenBidRequest(identity(), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, null); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesIsFalse() { + // given + final Boolean noCookies = false; + final BidRequest bidRequest = givenBidRequest( + identity(), + givenBannerImp(ExtImpAdnuntius.builder().network("network").noCookies(noCookies).build(), identity()), + givenBannerImp(identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, noCookies); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesIsTrue() { + // given + final Boolean noCookies = true; + final BidRequest bidRequest = givenBidRequest( + identity(), + givenBannerImp(ExtImpAdnuntius.builder().network("network").noCookies(noCookies).build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().noCookies(!noCookies).build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, noCookies); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithCorrectUriAndPopulateExtDeviceWithNoCookies() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.device(Device.builder().ext(givenExtDeviceNoCookies(null)).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, null); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtDeviceImpNoCookiesIsFalse() { + // given + final Boolean noCookies = false; + final BidRequest bidRequest = givenBidRequest( + request -> request.device(Device.builder().ext(givenExtDeviceNoCookies(noCookies)).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, noCookies); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtDeviceImpNoCookiesIsTrue() { + // given + final Boolean noCookies = true; + final BidRequest bidRequest = givenBidRequest( + request -> request.device(Device.builder().ext(givenExtDeviceNoCookies(noCookies)).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, noCookies); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithBasicUriIfGdprAndConsentAreAbsentWhenAlternativeUriProvided() { + // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + + final BidRequest bidRequest = givenBidRequest( + request -> request + .regs(Regs.builder().ext(ExtRegs.of(null, null, null, null)).build()) + .user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = buildExpectedUrl(ENDPOINT_URL, null, null, null); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithBasicUriIfGdprIsAbsentWhenAlternativeUriProvided() { + // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + + final BidRequest bidRequest = givenBidRequest( + request -> request + .regs(Regs.builder().ext(ExtRegs.of(null, null, null, null)).build()) + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final String expectedUrl = givenExpectedUrl(ENDPOINT_URL, null, "consent"); + + assertThat(result.getValue()) + .extracting(HttpRequest::getUri) + .containsExactly(expectedUrl, expectedUrl); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriIfConsentIsAbsentAndGdprIsPresent() { + // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final BidRequest bidRequest = givenBidRequest( request -> request .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(1, null); + final String expectedUrl = buildExpectedUrl(ALTERNATIVE_URL, 1, null, null); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -442,22 +776,52 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfConsentIsAbsent( } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUri() { + public void makeHttpRequestsShouldFailWhenGdprIsPresentAndAlternativeUriIsNotProvided() { + // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + null, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + + final BidRequest bidRequest = givenBidRequest( + request -> request + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) + .user(User.builder().ext(ExtUser.builder().consent(null).build()).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly(BidderError.badInput("an EU endpoint is required but invalid")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUri() { // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final Integer gdpr = 1; final String consent = "con sent"; final BidRequest bidRequest = givenBidRequest( request -> request .regs(Regs.builder().ext(ExtRegs.of(gdpr, null, null, null)).build()) .user(User.builder().ext(ExtUser.builder().consent(consent).build()).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(gdpr, consent); + final String expectedUrl = givenExpectedUrl(ALTERNATIVE_URL, gdpr, consent); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -466,17 +830,24 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUri() { } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesIsNull() { + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriIfExtImpNoCookiesIsNull() { // given - final BidRequest bidRequest = givenBidRequest(identity(), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity()), - givenImp(identity())); + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + + final BidRequest bidRequest = givenBidRequest( + request -> request.regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(null); + final String expectedUrl = buildExpectedUrl(ALTERNATIVE_URL, 1, null, null); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -485,19 +856,25 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesI } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesIsFalse() { + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriIfExtImpNoCookiesIsFalse() { // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final Boolean noCookies = false; final BidRequest bidRequest = givenBidRequest( - identity(), - givenImp(ExtImpAdnuntius.builder().network("network").noCookies(noCookies).build(), identity()), - givenImp(identity())); + request -> request.regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").noCookies(noCookies).build(), identity()), + givenBannerImp(identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(noCookies); + final String expectedUrl = givenExpectedUrl(ALTERNATIVE_URL, 1, noCookies); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -506,19 +883,25 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesI } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesIsTrue() { + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriIfExtImpNoCookiesIsTrue() { // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final Boolean noCookies = true; final BidRequest bidRequest = givenBidRequest( - identity(), - givenImp(ExtImpAdnuntius.builder().network("network").noCookies(noCookies).build(), identity()), - givenImp(ExtImpAdnuntius.builder().noCookies(!noCookies).build(), identity())); + request -> request.regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").noCookies(noCookies).build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().noCookies(!noCookies).build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(noCookies); + final String expectedUrl = givenExpectedUrl(ALTERNATIVE_URL, 1, noCookies); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -527,18 +910,26 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtImpNoCookiesI } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUriAndPopulateExtDeviceWithNoCookies() { + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriAndPopulateExtDeviceWithNoCookies() { // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final BidRequest bidRequest = givenBidRequest( - request -> request.device(Device.builder().ext(givenExtDeviceNoCookies(null)).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + request -> request + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) + .device(Device.builder().ext(givenExtDeviceNoCookies(null)).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(null); + final String expectedUrl = buildExpectedUrl(ALTERNATIVE_URL, 1, null, null); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -547,19 +938,27 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriAndPopulateExtDevi } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtDeviceImpNoCookiesIsFalse() { + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriIfExtDeviceImpNoCookiesIsFalse() { // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final Boolean noCookies = false; final BidRequest bidRequest = givenBidRequest( - request -> request.device(Device.builder().ext(givenExtDeviceNoCookies(noCookies)).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + request -> request + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) + .device(Device.builder().ext(givenExtDeviceNoCookies(noCookies)).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(noCookies); + final String expectedUrl = givenExpectedUrl(ALTERNATIVE_URL, 1, noCookies); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -568,19 +967,27 @@ public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtDeviceImpNoCo } @Test - public void makeHttpRequestsShouldReturnRequestsWithCorrectUriIfExtDeviceImpNoCookiesIsTrue() { + public void makeHttpRequestsShouldReturnRequestsWithAlternativeUriIfExtDeviceImpNoCookiesIsTrue() { // given + target = new AdnuntiusBidder( + ENDPOINT_URL, + ALTERNATIVE_URL, + Clock.system(ZoneId.of("UTC+05:00")), + jacksonMapper); + final Boolean noCookies = true; final BidRequest bidRequest = givenBidRequest( - request -> request.device(Device.builder().ext(givenExtDeviceNoCookies(noCookies)).build()), - givenImp(identity()), - givenImp(ExtImpAdnuntius.builder().network("network").build(), identity())); + request -> request + .regs(Regs.builder().ext(ExtRegs.of(1, null, null, null)).build()) + .device(Device.builder().ext(givenExtDeviceNoCookies(noCookies)).build()), + givenBannerImp(identity()), + givenBannerImp(ExtImpAdnuntius.builder().network("network").build(), identity())); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - final String expectedUrl = givenExpectedUrl(noCookies); + final String expectedUrl = givenExpectedUrl(ALTERNATIVE_URL, 1, noCookies); assertThat(result.getValue()) .extracting(HttpRequest::getUri) @@ -648,7 +1055,7 @@ public void makeBidsShouldReturnEmptyListIfResponseAdsUnitsIsEmpty() throws Json public void makeBidsShouldSkipInvalidAdsUnits() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall(givenAdsUnitWithAds("auId")); - final BidRequest bidRequest = givenBidRequest(givenImp(identity())); + final BidRequest bidRequest = givenBidRequest(givenBannerImp(identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -664,16 +1071,16 @@ public void makeBidsShouldUseCurrencyOfFirstBidOfLastRelatedImp() throws JsonPro final BidderCall httpCall = givenHttpCall( givenAdsUnitWithAds( "au1", - givenAd(ad -> ad.bid(AdnuntiusBid.of(null, "1.1"))), - givenAd(ad -> ad.bid(AdnuntiusBid.of(null, "1.2")))), + givenAd(ad -> ad.bid(AdnuntiusBid.of(BigDecimal.ONE, "1.1"))), + givenAd(ad -> ad.bid(AdnuntiusBid.of(BigDecimal.ONE, "1.2")))), givenAdsUnitWithAds( "au2", - givenAd(ad -> ad.bid(AdnuntiusBid.of(null, "2.1"))), - givenAd(ad -> ad.bid(AdnuntiusBid.of(null, "2.2"))))); + givenAd(ad -> ad.bid(AdnuntiusBid.of(BigDecimal.ONE, "2.1"))), + givenAd(ad -> ad.bid(AdnuntiusBid.of(BigDecimal.ONE, "2.2"))))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("au2").build(), identity()), - givenImp(ExtImpAdnuntius.builder().auId("au1").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("au2").build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().auId("au1").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -689,9 +1096,11 @@ public void makeBidsShouldUseCurrencyOfFirstBidOfLastRelatedImp() throws JsonPro public void makeBidsShouldPopulateGrossBidPriceWhenGrossBidSpecified() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - givenAdsUnitWithAds("auId", givenAd(ad -> ad.grossBid(AdnuntiusGrossBid.of(BigDecimal.ONE))))); + givenAdsUnitWithAds("auId", givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.TWO, "USD")) + .grossBid(AdnuntiusGrossBid.of(BigDecimal.ONE))))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("auId").bidType("gross").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("auId").bidType("gross").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -708,9 +1117,11 @@ public void makeBidsShouldPopulateGrossBidPriceWhenGrossBidSpecified() throws Js public void makeBidsShouldPopulateNetBidPriceWhenGrossBidSpecified() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - givenAdsUnitWithAds("auId", givenAd(ad -> ad.netBid(AdnuntiusNetBid.of(BigDecimal.ONE))))); + givenAdsUnitWithAds("auId", givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.TWO, "USD")) + .netBid(AdnuntiusNetBid.of(BigDecimal.ONE))))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("auId").bidType("net").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("auId").bidType("net").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -729,7 +1140,7 @@ public void makeBidsShouldNotReturnBidFromDealsWhenAdsIsAbsentAndDealsIsSpecifie final BidderCall httpCall = givenHttpCall(givenAdsUnitWithDeals("auId", givenAd(identity()))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -742,7 +1153,7 @@ public void makeBidsShouldNotReturnBidFromDealsWhenAdsIsAbsentAndDealsIsSpecifie @Test public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified() throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(givenAdsUnitWithDealsAndAds( + final BidderCall httpCall = givenHttpCall(givenBannerAdsUnitWithDealsAndAds( "auId", List.of(givenAd(ad -> ad .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) @@ -762,7 +1173,7 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( .advertiser(AdnuntiusAdvertiser.of("legalName", "name")) .advertiserDomains(List.of("domain1.com", "domain2.dt")))))); - final BidRequest bidRequest = givenBidRequest(givenImp( + final BidRequest bidRequest = givenBidRequest(givenBannerImp( ExtImpAdnuntius.builder().auId("auId").build(), identity())); // when @@ -780,7 +1191,87 @@ public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenAdsAndDealsIsSpecified( assertThat(bid).extracting(Bid::getCid).isEqualTo("lineItemId"); assertThat(bid).extracting(Bid::getDealid).isEqualTo("dealId"); assertThat(bid).extracting(Bid::getCrid).isEqualTo("creativeId"); + assertThat(bid).extracting(Bid::getMtype).isEqualTo(1); + assertThat(bid).extracting(Bid::getPrice).isEqualTo(BigDecimal.valueOf(1000)); + assertThat(bid).extracting(Bid::getAdomain).asList() + .containsExactlyInAnyOrder("domain1.com", "domain2.dt"); + assertThat(bid).extracting(Bid::getExt).isNull(); + }); + assertThat(bidderBid).extracting(BidderBid::getType).isEqualTo(BidType.banner); + assertThat(bidderBid).extracting(BidderBid::getBidCurrency).isEqualTo("USD"); + }); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnTwoBidFromDealsAndAdsWhenNativeAdsAndDealsIsSpecified() + throws JsonProcessingException { + + // given + final BidderCall httpCall = givenHttpCall(givenNativeAdsUnitWithDealsAndAds( + "auId", + List.of(givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .adId("adId") + .creativeId("creativeId") + .lineItemId("lineItemId") + .dealId("dealId") + .advertiser(AdnuntiusAdvertiser.of(null, "name")) + .advertiserDomains(List.of("domain1.com", "domain2.dt")))), + List.of(givenAd(ad -> ad + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .adId("adId") + .creativeId("creativeId") + .lineItemId("lineItemId") + .dealId("dealId") + .html("dealHtml") + .advertiser(AdnuntiusAdvertiser.of("legalName", "name")) + .advertiserDomains(List.of("domain1.com", "domain2.dt")))), + "{\"ortb\":{\"property\":\"value\"}}")); + + final BidRequest bidRequest = givenBidRequest(givenBannerImp( + ExtImpAdnuntius.builder().auId("auId").build(), identity())); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getValue()).hasSize(2); + + assertThat(result.getValue().getFirst()).satisfies(bidderBid -> { + assertThat(bidderBid).extracting(BidderBid::getBid).satisfies(bid -> { + assertThat(bid).extracting(Bid::getId).isEqualTo("adId"); + assertThat(bid).extracting(Bid::getImpid).isEqualTo("impId"); + assertThat(bid).extracting(Bid::getW).isEqualTo(21); + assertThat(bid).extracting(Bid::getH).isEqualTo(9); + assertThat(bid).extracting(Bid::getAdid).isEqualTo("adId"); + assertThat(bid).extracting(Bid::getAdm).isEqualTo("{\"property\":\"value\"}"); + assertThat(bid).extracting(Bid::getCid).isEqualTo("lineItemId"); + assertThat(bid).extracting(Bid::getDealid).isEqualTo("dealId"); + assertThat(bid).extracting(Bid::getCrid).isEqualTo("creativeId"); + assertThat(bid).extracting(Bid::getPrice).isEqualTo(BigDecimal.valueOf(1000)); + assertThat(bid).extracting(Bid::getMtype).isEqualTo(4); + assertThat(bid).extracting(Bid::getAdomain).asList() + .containsExactlyInAnyOrder("domain1.com", "domain2.dt"); + assertThat(bid).extracting(Bid::getExt).isNull(); + }); + assertThat(bidderBid).extracting(BidderBid::getType).isEqualTo(BidType.xNative); + assertThat(bidderBid).extracting(BidderBid::getBidCurrency).isEqualTo("USD"); + }); + + assertThat(result.getValue().getLast()).satisfies(bidderBid -> { + assertThat(bidderBid).extracting(BidderBid::getBid).satisfies(bid -> { + assertThat(bid).extracting(Bid::getId).isEqualTo("adId"); + assertThat(bid).extracting(Bid::getImpid).isEqualTo("impId"); + assertThat(bid).extracting(Bid::getW).isEqualTo(21); + assertThat(bid).extracting(Bid::getH).isEqualTo(9); + assertThat(bid).extracting(Bid::getAdid).isEqualTo("adId"); + assertThat(bid).extracting(Bid::getAdm).isEqualTo("dealHtml"); + assertThat(bid).extracting(Bid::getCid).isEqualTo("lineItemId"); + assertThat(bid).extracting(Bid::getDealid).isEqualTo("dealId"); + assertThat(bid).extracting(Bid::getCrid).isEqualTo("creativeId"); assertThat(bid).extracting(Bid::getPrice).isEqualTo(BigDecimal.valueOf(1000)); + assertThat(bid).extracting(Bid::getMtype).isEqualTo(1); assertThat(bid).extracting(Bid::getAdomain).asList() .containsExactlyInAnyOrder("domain1.com", "domain2.dt"); assertThat(bid).extracting(Bid::getExt).isNull(); @@ -796,7 +1287,7 @@ public void makeBidsShouldReturnTwoBidWithDsaFromDealsAndAdsWhenAdsAndDealsIsSpe throws JsonProcessingException { // given - final BidderCall httpCall = givenHttpCall(givenAdsUnitWithDealsAndAds( + final BidderCall httpCall = givenHttpCall(givenBannerAdsUnitWithDealsAndAds( "auId", List.of(givenAd(ad -> ad .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) @@ -823,7 +1314,7 @@ public void makeBidsShouldReturnTwoBidWithDsaFromDealsAndAdsWhenAdsAndDealsIsSpe final ExtRegsDsa dsa = ExtRegsDsa.of(1, 0, 2, null); final BidRequest bidRequest = givenBidRequest( request -> request.regs(Regs.builder().ext(ExtRegs.of(null, null, null, dsa)).build()), - givenImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -849,12 +1340,12 @@ public void makeBidsShouldReturnTwoBidWithDsaFromDealsAndAdsWhenAdsAndDealsIsSpe public void makeBidsShouldReturnErrorIfCreativeHeightOfSomeAdIsAbsent() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - givenAdsUnitWithAds("au1", givenAd(ad -> ad.bid(AdnuntiusBid.of(null, "CUR")))), + givenAdsUnitWithAds("au1", givenAd(ad -> ad.bid(AdnuntiusBid.of(BigDecimal.TWO, "CUR")))), givenAdsUnitWithAds("au2", givenAd(ad -> ad.creativeHeight(null)))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("au1").build(), identity()), - givenImp(ExtImpAdnuntius.builder().auId("au2").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("au1").build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().auId("au2").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -869,12 +1360,12 @@ public void makeBidsShouldReturnErrorIfCreativeHeightOfSomeAdIsAbsent() throws J public void makeBidsShouldReturnErrorIfCreativeWidthtOfSomeAdIsAbsent() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - givenAdsUnitWithAds("au1", givenAd(ad -> ad.bid(AdnuntiusBid.of(null, "CUR")))), + givenAdsUnitWithAds("au1", givenAd(ad -> ad.bid(AdnuntiusBid.of(BigDecimal.TWO, "CUR")))), givenAdsUnitWithAds("au2", givenAd(ad -> ad.creativeWidth(null)))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("au1").build(), identity()), - givenImp(ExtImpAdnuntius.builder().auId("au2").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("au1").build(), identity()), + givenBannerImp(ExtImpAdnuntius.builder().auId("au2").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -899,7 +1390,7 @@ public void makeBidsShouldReturnCorrectSeatBids() throws JsonProcessingException .advertiserDomains(List.of("domain1.com", "domain2.dt"))))); final BidRequest bidRequest = givenBidRequest( - givenImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); + givenBannerImp(ExtImpAdnuntius.builder().auId("auId").build(), identity())); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -934,14 +1425,31 @@ private BidRequest givenBidRequest(Imp... imps) { return givenBidRequest(identity(), imps); } - private Imp givenImp(ExtImpAdnuntius extImpAdnuntius, UnaryOperator impCustomizer) { + private Imp givenBannerAndNativeImp(ExtImpAdnuntius extImpAdnuntius, UnaryOperator impCustomizer) { + final Banner banner = Banner.builder().build(); + final Native xNative = Native.builder().request("{}").build(); + final ObjectNode ext = mapper.valueToTree(ExtPrebid.of(null, extImpAdnuntius)); + return impCustomizer.apply(Imp.builder().id("impId").banner(banner).xNative(xNative).ext(ext)).build(); + } + + private Imp givenBannerImp(ExtImpAdnuntius extImpAdnuntius, UnaryOperator impCustomizer) { final Banner banner = Banner.builder().build(); final ObjectNode ext = mapper.valueToTree(ExtPrebid.of(null, extImpAdnuntius)); return impCustomizer.apply(Imp.builder().id("impId").banner(banner).ext(ext)).build(); } - private Imp givenImp(UnaryOperator impCustomizer) { - return givenImp(ExtImpAdnuntius.builder().build(), impCustomizer); + private Imp givenBannerImp(UnaryOperator impCustomizer) { + return givenBannerImp(ExtImpAdnuntius.builder().build(), impCustomizer); + } + + private Imp givenNativeImp(ExtImpAdnuntius extImpAdnuntius, UnaryOperator impCustomizer) { + final Native xNative = Native.builder().request("{}").build(); + final ObjectNode ext = mapper.valueToTree(ExtPrebid.of(null, extImpAdnuntius)); + return impCustomizer.apply(Imp.builder().id("impId").xNative(xNative).ext(ext)).build(); + } + + private Imp givenNativeImp(UnaryOperator impCustomizer) { + return givenNativeImp(ExtImpAdnuntius.builder().build(), impCustomizer); } private BidderCall givenHttpCall(String body) { @@ -950,35 +1458,55 @@ private BidderCall givenHttpCall(String body) { return BidderCall.succeededHttp(request, response, null); } - private BidderCall givenHttpCall(AdnuntiusAdsUnit... adsUnits) + private BidderCall givenHttpCall(AdnuntiusAdUnit... adsUnits) throws JsonProcessingException { return givenHttpCall(mapper.writeValueAsString(AdnuntiusResponse.of(List.of(adsUnits)))); } - private AdnuntiusAdsUnit givenAdsUnitWithAds(String auId, AdnuntiusAd... ads) { - return givenAdsUnit(auId, List.of(ads), null); + private AdnuntiusAdUnit givenAdsUnitWithAds(String auId, AdnuntiusAd... ads) { + return givenAdsUnit(auId, List.of(ads), null, null); } - private AdnuntiusAdsUnit givenAdsUnitWithDeals(String auId, AdnuntiusAd... deals) { - return givenAdsUnit(auId, null, List.of(deals)); + private AdnuntiusAdUnit givenAdsUnitWithDeals(String auId, AdnuntiusAd... deals) { + return givenAdsUnit(auId, null, List.of(deals), null); } - private AdnuntiusAdsUnit givenAdsUnitWithDealsAndAds(String auId, List ads, List deals) { - return givenAdsUnit(auId, ads, deals); + private AdnuntiusAdUnit givenBannerAdsUnitWithDealsAndAds(String auId, + List ads, + List deals) { + return givenAdsUnit(auId, ads, deals, null); } - private AdnuntiusAdsUnit givenAdsUnit(String auId, List ads, List deals) { - return AdnuntiusAdsUnit.builder() + private AdnuntiusAdUnit givenNativeAdsUnitWithDealsAndAds(String auId, + List ads, + List deals, + String nativeJson) { + return givenAdsUnit(auId, ads, deals, nativeJson); + } + + private AdnuntiusAdUnit givenAdsUnit(String auId, + List ads, + List deals, + String nativeJson) { + + return AdnuntiusAdUnit.builder() .auId(auId) .targetId(auId + "-impId") .html("html") .ads(ads) .deals(deals) + .matchedAdCount(1) + .nativeJson(nativeJson != null + ? jacksonMapper.decodeValue(nativeJson, AdnuntiusNativeRequest.class) + : null) .build(); } private AdnuntiusAd givenAd(UnaryOperator customizer) { - return customizer.apply(AdnuntiusAd.builder().creativeWidth("21").creativeHeight("9")).build(); + return customizer.apply(AdnuntiusAd.builder() + .bid(AdnuntiusBid.of(BigDecimal.ONE, "USD")) + .creativeWidth("21") + .creativeHeight("9")).build(); } private static ExtDevice givenExtDeviceNoCookies(Boolean noCookies) { @@ -989,16 +1517,20 @@ private static ExtDevice givenExtDeviceNoCookies(Boolean noCookies) { return extDevice; } - private static String givenExpectedUrl(Integer gdpr, String consent) { - return buildExpectedUrl(gdpr, consent, false); + private static String givenExpectedUrl(String url, Integer gdpr, String consent) { + return buildExpectedUrl(url, gdpr, consent, false); + } + + private static String givenExpectedUrl(String url, Integer gdpr, Boolean noCookies) { + return buildExpectedUrl(url, gdpr, null, noCookies); } - private static String givenExpectedUrl(Boolean noCookies) { - return buildExpectedUrl(null, null, noCookies); + private static String givenExpectedUrl(String url, Boolean noCookies) { + return buildExpectedUrl(url, null, null, noCookies); } - private static String buildExpectedUrl(Integer gdpr, String consent, Boolean noCookies) { - final StringBuilder expectedUri = new StringBuilder("https://test.domain.dm/uri?format=prebidServer&tzo=-300"); + private static String buildExpectedUrl(String url, Integer gdpr, String consent, Boolean noCookies) { + final StringBuilder expectedUri = new StringBuilder(url + "?format=prebidServer&tzo=-300"); if (gdpr != null) { expectedUri.append("&gdpr=").append(HttpUtil.encodeUrl(gdpr.toString())); } diff --git a/src/test/java/org/prebid/server/it/AdnuntiusTest.java b/src/test/java/org/prebid/server/it/AdnuntiusTest.java index 9a03d73ccf5..b2847d98dda 100644 --- a/src/test/java/org/prebid/server/it/AdnuntiusTest.java +++ b/src/test/java/org/prebid/server/it/AdnuntiusTest.java @@ -18,7 +18,7 @@ public class AdnuntiusTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromAdnuntius() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adnuntius-exchange")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adnuntius-exchange-eu")) .withRequestBody(equalToJson(jsonFrom("openrtb2/adnuntius/test-adnuntius-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/adnuntius/test-adnuntius-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json index c0707bf2a61..7d075275a7b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-request.json @@ -2,7 +2,7 @@ "adUnits": [ { "auId": "some_au_id", - "targetId": "some_au_id-imp_id", + "targetId": "some_au_id-imp_id:banner", "dimensions": [ [ 300, diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-response.json index 4020376e20f..3f74ebaa9f0 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-adnuntius-bid-response.json @@ -2,7 +2,8 @@ "adUnits": [ { "auId": "some_au_id", - "targetId": "some_au_id-imp_id", + "targetId": "some_au_id-imp_id:banner", + "matchedAdCount": 1, "html": "some_html", "ads": [ { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json index 6d033a9c208..648eef6866a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json @@ -17,6 +17,7 @@ "crid": "some_creative_id", "dealid": "some_deal_id", "w": 140, + "mtype": 1, "h": 90, "ext": { "prebid": { 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 5216ec30fc8..d063f6c1da4 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -51,6 +51,7 @@ adapters.admixer.enabled=true adapters.admixer.endpoint=http://localhost:8090/admixer-exchange adapters.adnuntius.enabled=true adapters.adnuntius.endpoint=http://localhost:8090/adnuntius-exchange +adapters.adnuntius.eu-endpoint=http://localhost:8090/adnuntius-exchange-eu adapters.adocean.enabled=true adapters.adocean.endpoint=http://localhost:8090/adocean-exchange adapters.adoppler.enabled=true From 8a86567eae077ae2e00cdad6022d751dc45d692d Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:51:30 +0200 Subject: [PATCH 20/51] Adjust Floors for Bidadjustments (#3910) --- .../prebid/server/auction/BidsAdjuster.java | 8 +- .../server/auction/ExchangeService.java | 25 +- .../server/auction/model/AuctionContext.java | 12 - .../server/auction/model/BidderRequest.java | 5 + .../requestfactory/AuctionRequestFactory.java | 10 +- .../BidAdjustmentFactorResolver.java | 3 +- .../BidAdjustmentRulesValidator.java | 20 +- .../BidAdjustmentsEnricher.java | 105 ++ .../BidAdjustmentsProcessor.java | 32 +- .../BidAdjustmentsResolver.java | 62 +- .../BidAdjustmentsRetriever.java | 86 -- .../BidAdjustmentsRulesResolver.java | 94 ++ .../FloorAdjustmentFactorResolver.java | 3 +- .../FloorAdjustmentsResolver.java | 89 ++ .../bidadjustments/model/BidAdjustments.java | 43 +- .../model/BidAdjustmentsRule.java} | 5 +- .../model/BidAdjustmentsRules.java | 50 + .../floors/BasicPriceFloorAdjuster.java | 73 +- .../floors/BasicPriceFloorEnforcer.java | 37 +- .../NoSignalBidderPriceFloorAdjuster.java | 5 - .../server/floors/PriceFloorAdjuster.java | 7 - .../ext/request/ExtRequestBidAdjustments.java | 15 - .../config/PriceFloorsConfiguration.java | 24 +- .../spring/config/ServiceConfiguration.java | 27 +- .../impl/MostAccurateCombinationStrategy.java | 4 +- .../request/auction/BidAdjustmentRule.groovy | 9 +- .../model/request/auction/BidRequest.groovy | 5 + .../model/response/auction/Bid.groovy | 30 + .../response/auction/BidMediaType.groovy | 13 + .../service/PrebidServerService.groovy | 8 +- .../testcontainers/PbsConfig.groovy | 11 +- .../scaffolding/CurrencyConversion.groovy | 4 +- .../server/functional/tests/BaseSpec.groovy | 6 + .../functional/tests/BidAdjustmentSpec.groovy | 620 +++++++-- .../functional/tests/CurrencySpec.groovy | 69 +- .../tests/StoredResponseSpec.groovy | 12 +- .../PriceFloorsAdjustmentSpec.groovy | 1240 +++++++++++++++++ .../pricefloors/PriceFloorsBaseSpec.groovy | 22 +- .../PriceFloorsCurrencySpec.groovy | 75 +- .../PriceFloorsSignalingSpec.groovy | 5 + .../functional/util/CurrencyUtil.groovy | 74 + .../server/auction/BidsAdjusterTest.java | 4 +- .../server/auction/ExchangeServiceTest.java | 2 + .../AuctionRequestFactoryTest.java | 43 +- .../BidAdjustmentFactorResolverTest.java | 4 +- .../BidAdjustmentRulesValidatorTest.java | 123 +- ...t.java => BidAdjustmentsEnricherTest.java} | 169 ++- .../BidAdjustmentsProcessorTest.java | 128 +- .../BidAdjustmentsResolverTest.java | 123 +- .../BidAdjustmentsRulesResolverTest.java | 694 +++++++++ .../FloorAdjustmentFactorResolverTest.java | 2 +- .../FloorAdjustmentsResolverTest.java | 235 ++++ ...Test.java => BidAdjustmentsRulesTest.java} | 28 +- .../floors/BasicPriceFloorAdjusterTest.java | 233 +--- .../floors/BasicPriceFloorEnforcerTest.java | 62 +- .../NoSignalBidderPriceFloorAdjusterTest.java | 19 - 56 files changed, 3836 insertions(+), 1080 deletions(-) rename src/main/java/org/prebid/server/{auction/adjustment => bidadjustments}/BidAdjustmentFactorResolver.java (94%) create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java delete mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java rename src/main/java/org/prebid/server/{auction/adjustment => bidadjustments}/FloorAdjustmentFactorResolver.java (94%) create mode 100644 src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java rename src/main/java/org/prebid/server/{proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java => bidadjustments/model/BidAdjustmentsRule.java} (71%) create mode 100644 src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java delete mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java create mode 100644 src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy rename src/test/java/org/prebid/server/{auction/adjustment => bidadjustments}/BidAdjustmentFactorResolverTest.java (98%) rename src/test/java/org/prebid/server/bidadjustments/{BidAdjustmentsRetrieverTest.java => BidAdjustmentsEnricherTest.java} (65%) create mode 100644 src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java rename src/test/java/org/prebid/server/{auction/adjustment => bidadjustments}/FloorAdjustmentFactorResolverTest.java (99%) create mode 100644 src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java rename src/test/java/org/prebid/server/bidadjustments/model/{BidAdjustmentsTest.java => BidAdjustmentsRulesTest.java} (65%) diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java index f79b6dd7e6c..63b0f4b6db0 100644 --- a/src/main/java/org/prebid/server/auction/BidsAdjuster.java +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -43,21 +43,21 @@ public List validateAndAdjustBids(List validBidderResponse(auctionParticipation, auctionContext, aliases)) .map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids( auctionParticipation, - auctionContext.getBidRequest(), - auctionContext.getBidAdjustments())) + bidRequest)) .map(auctionParticipation -> priceFloorEnforcer.enforce( - auctionContext.getBidRequest(), + bidRequest, auctionParticipation, auctionContext.getAccount(), auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) .map(auctionParticipation -> dsaEnforcer.enforce( - auctionContext.getBidRequest(), + bidRequest, auctionParticipation, auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) .toList(); diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 1db72fcc649..04191d0839d 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -96,6 +96,7 @@ import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountCacheConfig; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ListUtil; import org.prebid.server.util.PbsUtil; @@ -739,20 +740,34 @@ private AuctionParticipation createAuctionParticipation( final String storedBidResponse = impBidderToStoredBidResponse.size() == 1 ? impBidderToStoredBidResponse.get(imps.getFirst().getId()).get(bidder) : null; + + final BidRequest enrichedWithPriceFloors = priceFloorProcessor.enrichWithPriceFloors( + context.getBidRequest().toBuilder().imp(imps).build(), + context.getAccount(), + bidder, + context.getPrebidErrors(), + context.getDebugWarnings()); + final BidRequest preparedBidRequest = prepareBidRequest( bidderPrivacyResult, - imps, + enrichedWithPriceFloors, bidderToMultiBid, biddersToConfigs, bidderToPrebidBidders, bidderAliases, context); + final Map originalPriceFloors = enrichedWithPriceFloors.getImp().stream() + .filter(imp -> BidderUtil.isValidPrice(imp.getBidfloor()) + && StringUtils.isNotBlank(imp.getBidfloorcur())) + .collect(Collectors.toMap(Imp::getId, imp -> Price.of(imp.getBidfloorcur(), imp.getBidfloor()))); + final BidderRequest bidderRequest = BidderRequest.builder() .bidder(bidder) .ortbVersion(ortbVersion) .storedResponse(storedBidResponse) .bidRequest(preparedBidRequest) + .originalPriceFloors(originalPriceFloors) .build(); return AuctionParticipation.builder() @@ -768,7 +783,7 @@ private OrtbVersion bidderSupportedOrtbVersion(String bidder, BidderAliases alia } private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, - List imps, + BidRequest bidRequest, Map bidderToMultiBid, Map biddersToConfigs, Map bidderToPrebidBidders, @@ -776,12 +791,6 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, AuctionContext context) { final String bidder = bidderPrivacyResult.getRequestBidder(); - final BidRequest bidRequest = priceFloorProcessor.enrichWithPriceFloors( - context.getBidRequest().toBuilder().imp(imps).build(), - context.getAccount(), - bidder, - context.getPrebidErrors(), - context.getDebugWarnings()); final boolean transmitTid = transmitTransactionId(bidder, context); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.stream() diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 5dbe83c3ff2..3ee60aab4fa 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -8,7 +8,6 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.debug.DebugContext; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.geolocation.model.GeoInfo; @@ -18,7 +17,6 @@ import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -73,10 +71,6 @@ public class AuctionContext { CachedDebugLog cachedDebugLog; - @JsonIgnore - @Builder.Default - BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap()); - public AuctionContext with(Account account) { return this.toBuilder().account(account).build(); } @@ -130,12 +124,6 @@ public AuctionContext with(GeoInfo geoInfo) { .build(); } - public AuctionContext with(BidAdjustments bidAdjustments) { - return this.toBuilder() - .bidAdjustments(bidAdjustments) - .build(); - } - public AuctionContext withRequestRejected() { return this.toBuilder() .requestRejected(true) diff --git a/src/main/java/org/prebid/server/auction/model/BidderRequest.java b/src/main/java/org/prebid/server/auction/model/BidderRequest.java index ad60230e54b..1e2ac58a8db 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderRequest.java +++ b/src/main/java/org/prebid/server/auction/model/BidderRequest.java @@ -4,6 +4,9 @@ import lombok.Builder; import lombok.Value; import org.prebid.server.auction.versionconverter.OrtbVersion; +import org.prebid.server.bidder.model.Price; + +import java.util.Map; @Builder(toBuilder = true) @Value @@ -17,6 +20,8 @@ public class BidderRequest { BidRequest bidRequest; + Map originalPriceFloors; + public BidderRequest with(BidRequest bidRequest) { return toBuilder().bidRequest(bidRequest).build(); } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 1edf2fbed39..628ea212fd8 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -17,7 +17,7 @@ import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; -import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; @@ -51,7 +51,7 @@ public class AuctionRequestFactory { private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; private final GeoLocationServiceWrapper geoLocationServiceWrapper; - private final BidAdjustmentsRetriever bidAdjustmentsRetriever; + private final BidAdjustmentsEnricher bidAdjustmentsEnricher; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); @@ -69,7 +69,7 @@ public AuctionRequestFactory(long maxRequestSize, DebugResolver debugResolver, JacksonMapper mapper, GeoLocationServiceWrapper geoLocationServiceWrapper, - BidAdjustmentsRetriever bidAdjustmentsRetriever) { + BidAdjustmentsEnricher bidAdjustmentsEnricher) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); @@ -85,7 +85,7 @@ public AuctionRequestFactory(long maxRequestSize, this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); - this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever); + this.bidAdjustmentsEnricher = Objects.requireNonNull(bidAdjustmentsEnricher); } /** @@ -146,7 +146,7 @@ public Future enrichAuctionContext(AuctionContext initialContext .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) .map(auctionContext::with)) - .map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext))) + .map(auctionContext -> auctionContext.with(bidAdjustmentsEnricher.enrichBidRequest(auctionContext))) .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) diff --git a/src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java similarity index 94% rename from src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java rename to src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java index 3f41b29110f..8218bfd8547 100644 --- a/src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -38,6 +38,7 @@ private static Optional resolveFromMediaTypes( } return Optional.ofNullable(mediaType) + .map(type -> type == ImpMediaType.video_instream ? ImpMediaType.video : type) .map(adjustmentFactors::get) .flatMap(factors -> factors.entrySet().stream() .filter(entry -> StringUtils.equalsIgnoreCase(entry.getKey(), bidderCode)) diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java index 9ddeefb6e2e..34495d2cea3 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java @@ -3,8 +3,8 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidadjustments.model.BidAdjustmentType; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.validation.ValidationException; @@ -16,7 +16,7 @@ public class BidAdjustmentRulesValidator { public static final Set SUPPORTED_MEDIA_TYPES = Set.of( - BidAdjustmentsResolver.WILDCARD, + BidAdjustmentsRulesResolver.WILDCARD, ImpMediaType.banner.toString(), ImpMediaType.audio.toString(), ImpMediaType.video_instream.toString(), @@ -27,13 +27,13 @@ private BidAdjustmentRulesValidator() { } - public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException { + public static void validate(BidAdjustments bidAdjustments) throws ValidationException { if (bidAdjustments == null) { return; } - final Map>>> mediatypes = - bidAdjustments.getMediatype(); + final Map>>> mediatypes = + bidAdjustments.getRules(); if (MapUtils.isEmpty(mediatypes)) { return; @@ -41,12 +41,12 @@ public static void validate(ExtRequestBidAdjustments bidAdjustments) throws Vali for (String mediatype : mediatypes.keySet()) { if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) { - final Map>> bidders = mediatypes.get(mediatype); + final Map>> bidders = mediatypes.get(mediatype); if (MapUtils.isEmpty(bidders)) { throw new ValidationException("no bidders found in %s".formatted(mediatype)); } for (String bidder : bidders.keySet()) { - final Map> deals = bidders.get(bidder); + final Map> deals = bidders.get(bidder); if (MapUtils.isEmpty(deals)) { throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder)); @@ -61,14 +61,14 @@ public static void validate(ExtRequestBidAdjustments bidAdjustments) throws Vali } } - private static void validateRules(List rules, + private static void validateRules(List rules, String path) throws ValidationException { if (rules == null) { throw new ValidationException("no bid adjustment rules found in %s".formatted(path)); } - for (ExtRequestBidAdjustmentsRule rule : rules) { + for (BidAdjustmentsRule rule : rules) { final BidAdjustmentType type = rule.getAdjType(); final String currency = rule.getCurrency(); final BigDecimal value = rule.getValue(); diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java new file mode 100644 index 00000000000..7ac48388c2e --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java @@ -0,0 +1,105 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.validation.ValidationException; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsEnricher { + + private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsEnricher.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final ObjectMapper mapper; + private final JacksonMapper jacksonMapper; + private final JsonMerger jsonMerger; + private final double samplingRate; + + public BidAdjustmentsEnricher(JacksonMapper mapper, JsonMerger jsonMerger, double samplingRate) { + this.jacksonMapper = Objects.requireNonNull(mapper); + this.mapper = mapper.mapper(); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.samplingRate = samplingRate; + } + + public BidRequest enrichBidRequest(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final List debugWarnings = auctionContext.getDebugWarnings(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + + final JsonNode requestNode = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode accountNode = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidAdjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode mergedNode = jsonMerger.merge(requestNode, accountNode); + + final List resolvedWarnings = debugEnabled ? debugWarnings : null; + final JsonNode resolvedBidAdjustments = convertAndValidate(mergedNode, resolvedWarnings, "request") + .or(() -> convertAndValidate(accountNode, resolvedWarnings, "account")) + .orElse(null); + + return bidRequest.toBuilder() + .ext(updateExtRequestWithBidAdjustments(bidRequest, resolvedBidAdjustments)) + .build(); + } + + private Optional convertAndValidate(JsonNode bidAdjustmentsNode, + List debugWarnings, + String errorLocation) { + + if (bidAdjustmentsNode.isEmpty()) { + return Optional.empty(); + } + + try { + final BidAdjustments bidAdjustments = mapper.convertValue(bidAdjustmentsNode, BidAdjustments.class); + + BidAdjustmentRulesValidator.validate(bidAdjustments); + return Optional.of(bidAdjustmentsNode); + } catch (IllegalArgumentException | ValidationException e) { + final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); + if (debugWarnings != null) { + debugWarnings.add(message); + } + conditionalLogger.error(message, samplingRate); + return Optional.empty(); + } + } + + private ExtRequest updateExtRequestWithBidAdjustments(BidRequest bidRequest, JsonNode bidAdjustments) { + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid updatedPrebid = Optional.ofNullable(extRequest) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::toBuilder) + .orElse(ExtRequestPrebid.builder()) + .bidadjustments((ObjectNode) bidAdjustments) + .build(); + + final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); + return extRequest == null + ? updatedExtRequest + : jacksonMapper.fillExtension(updatedExtRequest, extRequest.getProperties()); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java index f419a0ad4d4..c161dcce193 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -1,6 +1,7 @@ package org.prebid.server.bidadjustments; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -8,10 +9,8 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.ImpMediaTypeResolver; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; @@ -42,7 +41,7 @@ public class BidAdjustmentsProcessor { private final CurrencyConversionService currencyService; private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; private final BidAdjustmentsResolver bidAdjustmentsResolver; - private final JacksonMapper mapper; + private final ObjectMapper mapper; public BidAdjustmentsProcessor(CurrencyConversionService currencyService, BidAdjustmentFactorResolver bidAdjustmentFactorResolver, @@ -52,12 +51,11 @@ public BidAdjustmentsProcessor(CurrencyConversionService currencyService, this.currencyService = Objects.requireNonNull(currencyService); this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); this.bidAdjustmentsResolver = Objects.requireNonNull(bidAdjustmentsResolver); - this.mapper = Objects.requireNonNull(mapper); + this.mapper = Objects.requireNonNull(mapper).mapper(); } public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionParticipation, - BidRequest bidRequest, - BidAdjustments bidAdjustments) { + BidRequest bidRequest) { if (auctionParticipation.isRequestBlocked()) { return auctionParticipation; @@ -75,7 +73,7 @@ public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionP final String bidder = auctionParticipation.getBidder(); final List updatedBidderBids = bidderBids.stream() - .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, bidAdjustments, errors)) + .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, errors)) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -90,7 +88,6 @@ public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionP private BidderBid applyBidAdjustments(BidderBid bidderBid, BidRequest bidRequest, String bidder, - BidAdjustments bidAdjustments, List errors) { try { final Price originalPrice = getOriginalPrice(bidderBid); @@ -109,9 +106,9 @@ private BidderBid applyBidAdjustments(BidderBid bidderBid, final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules( priceWithFactorsApplied, + bidderBid.getSeat(), bidder, bidRequest, - bidAdjustments, mediaType, bidderBid.getBid().getDealid()); @@ -133,7 +130,7 @@ private String getAdapterCode(Bid bid) { private ExtBidPrebid convertValue(JsonNode jsonNode) { try { - return mapper.mapper().convertValue(jsonNode.get(PREBID_EXT), ExtBidPrebid.class); + return mapper.convertValue(jsonNode.get(PREBID_EXT), ExtBidPrebid.class); } catch (IllegalArgumentException ignored) { return null; } @@ -142,7 +139,7 @@ private ExtBidPrebid convertValue(JsonNode jsonNode) { private BidderBid updateBid(Price originalPrice, Price adjustedPrice, BidderBid bidderBid, BidRequest bidRequest) { final Bid bid = bidderBid.getBid(); final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.createObjectNode(); final BigDecimal originalBidPrice = originalPrice.getValue(); final String originalBidCurrency = originalPrice.getCurrency(); @@ -196,12 +193,9 @@ private BigDecimal bidAdjustmentForBidder(String bidder, ImpMediaType mediaType) { final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - - final ImpMediaType targetMediaType = mediaType == ImpMediaType.video_instream ? ImpMediaType.video : mediaType; - return bidAdjustmentFactorResolver.resolve(targetMediaType, adjustmentFactors, bidder, seat); + return adjustmentFactors == null + ? null + : bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder, seat); } private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { @@ -216,17 +210,17 @@ private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecim } private Price applyBidAdjustmentRules(Price bidPrice, + String seat, String bidder, BidRequest bidRequest, - BidAdjustments bidAdjustments, ImpMediaType mediaType, String dealId) { return bidAdjustmentsResolver.resolve( bidPrice, bidRequest, - bidAdjustments, mediaType, + seat, bidder, dealId); } diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java index ffac1cbc51a..46947c40e67 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java @@ -1,88 +1,50 @@ package org.prebid.server.bidadjustments; import com.iab.openrtb.request.BidRequest; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidadjustments.model.BidAdjustmentType; -import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.bidder.model.Price; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; -import org.prebid.server.util.dsl.config.PrebidConfigParameter; -import org.prebid.server.util.dsl.config.PrebidConfigParameters; -import org.prebid.server.util.dsl.config.PrebidConfigSource; -import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; -import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; -import org.prebid.server.util.dsl.config.impl.SimpleParameters; -import org.prebid.server.util.dsl.config.impl.SimpleSource; import java.math.BigDecimal; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; public class BidAdjustmentsResolver { - public static final String WILDCARD = "*"; - public static final String DELIMITER = "|"; - - private final PrebidConfigMatchingStrategy matchingStrategy; private final CurrencyConversionService currencyService; + private final BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + + public BidAdjustmentsResolver(CurrencyConversionService currencyService, + BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver) { - public BidAdjustmentsResolver(CurrencyConversionService currencyService) { this.currencyService = Objects.requireNonNull(currencyService); - this.matchingStrategy = new MostAccurateCombinationStrategy(); + this.bidAdjustmentsRulesResolver = Objects.requireNonNull(bidAdjustmentsRulesResolver); } public Price resolve(Price initialPrice, BidRequest bidRequest, - BidAdjustments bidAdjustments, ImpMediaType targetMediaType, + String targetSeat, String targetBidder, String targetDealId) { - final List adjustmentsRules = findRules( - bidAdjustments, - targetMediaType, - targetBidder, - targetDealId); - - return adjustPrice(initialPrice, adjustmentsRules, bidRequest); - } - - private List findRules(BidAdjustments bidAdjustments, - ImpMediaType targetMediaType, - String targetBidder, - String targetDealId) { - - final Map> rules = bidAdjustments.getRules(); - final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); - final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId); - - final String rule = matchingStrategy.match(source, parameters); - return rule == null ? Collections.emptyList() : rules.get(rule); - } - - private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) { - final List conditionsMatchers = List.of( - SimpleDirectParameter.of(mediaType.toString()), - SimpleDirectParameter.of(bidder), - StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard()); + final List rules = bidAdjustmentsRulesResolver.resolve( + bidRequest, targetMediaType, targetSeat, targetBidder, targetDealId); - return SimpleParameters.of(conditionsMatchers); + return adjustPrice(initialPrice, rules, bidRequest); } private Price adjustPrice(Price price, - List bidAdjustmentRules, + List bidAdjustmentRules, BidRequest bidRequest) { String resolvedCurrency = price.getCurrency(); BigDecimal resolvedPrice = price.getValue(); - for (ExtRequestBidAdjustmentsRule rule : bidAdjustmentRules) { + for (BidAdjustmentsRule rule : bidAdjustmentRules) { final BidAdjustmentType adjustmentType = rule.getAdjType(); final BigDecimal adjustmentValue = rule.getValue(); final String adjustmentCurrency = rule.getCurrency(); diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java deleted file mode 100644 index 6a151754bb2..00000000000 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.prebid.server.bidadjustments; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.iab.openrtb.request.BidRequest; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.bidadjustments.model.BidAdjustments; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.JsonMerger; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.Logger; -import org.prebid.server.log.LoggerFactory; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAuctionConfig; -import org.prebid.server.validation.ValidationException; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class BidAdjustmentsRetriever { - - private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsRetriever.class); - private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); - - private final ObjectMapper mapper; - private final JsonMerger jsonMerger; - private final double samplingRate; - - public BidAdjustmentsRetriever(JacksonMapper mapper, - JsonMerger jsonMerger, - double samplingRate) { - this.mapper = Objects.requireNonNull(mapper).mapper(); - this.jsonMerger = Objects.requireNonNull(jsonMerger); - this.samplingRate = samplingRate; - } - - public BidAdjustments retrieve(AuctionContext auctionContext) { - final List debugWarnings = auctionContext.getDebugWarnings(); - final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); - - final JsonNode requestBidAdjustmentsNode = Optional.ofNullable(auctionContext.getBidRequest()) - .map(BidRequest::getExt) - .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getBidadjustments) - .orElseGet(mapper::createObjectNode); - - final JsonNode accountBidAdjustmentsNode = Optional.ofNullable(auctionContext.getAccount()) - .map(Account::getAuction) - .map(AccountAuctionConfig::getBidAdjustments) - .orElseGet(mapper::createObjectNode); - - final JsonNode mergedBidAdjustmentsNode = jsonMerger.merge( - requestBidAdjustmentsNode, - accountBidAdjustmentsNode); - - final List resolvedWarnings = debugEnabled ? debugWarnings : null; - return convertAndValidate(mergedBidAdjustmentsNode, resolvedWarnings, "request") - .or(() -> convertAndValidate(accountBidAdjustmentsNode, resolvedWarnings, "account")) - .orElse(BidAdjustments.of(Collections.emptyMap())); - } - - private Optional convertAndValidate(JsonNode bidAdjustmentsNode, - List debugWarnings, - String errorLocation) { - try { - final ExtRequestBidAdjustments accountBidAdjustments = mapper.convertValue( - bidAdjustmentsNode, - ExtRequestBidAdjustments.class); - - BidAdjustmentRulesValidator.validate(accountBidAdjustments); - return Optional.of(BidAdjustments.of(accountBidAdjustments)); - } catch (IllegalArgumentException | ValidationException e) { - final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); - if (debugWarnings != null) { - debugWarnings.add(message); - } - conditionalLogger.error(message, samplingRate); - return Optional.empty(); - } - } -} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java new file mode 100644 index 00000000000..d133d997520 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java @@ -0,0 +1,94 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; +import org.prebid.server.util.dsl.config.PrebidConfigParameter; +import org.prebid.server.util.dsl.config.PrebidConfigParameters; +import org.prebid.server.util.dsl.config.PrebidConfigSource; +import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; +import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; +import org.prebid.server.util.dsl.config.impl.SimpleParameters; +import org.prebid.server.util.dsl.config.impl.SimpleSource; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsRulesResolver { + + public static final String WILDCARD = "*"; + public static final String DELIMITER = "|"; + + private final PrebidConfigMatchingStrategy matchingStrategy; + private final ObjectMapper mapper; + + public BidAdjustmentsRulesResolver(JacksonMapper mapper) { + this.matchingStrategy = new MostAccurateCombinationStrategy(); + this.mapper = Objects.requireNonNull(mapper).mapper(); + } + + public List resolve(BidRequest bidRequest, ImpMediaType targetMediaType, String targetBidder) { + return resolve(bidRequest, targetMediaType, null, targetBidder, null); + } + + public List resolve(BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetSeat, + String targetBidder, + String targetDealId) { + + final BidAdjustmentsRules bidAdjustments = BidAdjustmentsRules.of(extractBidAdjustments(bidRequest)); + return findRules(bidAdjustments, targetMediaType, targetSeat, targetBidder, targetDealId); + } + + private BidAdjustments extractBidAdjustments(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .map(node -> mapper.convertValue(node, BidAdjustments.class)) + .orElse(null); + } + + private List findRules(BidAdjustmentsRules bidAdjustments, + ImpMediaType targetMediaType, + String targetSeat, + String targetBidder, + String targetDealId) { + + final Map> rules = bidAdjustments.getRules(); + final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); + final PrebidConfigParameters parameters = createParameters( + targetMediaType, targetSeat, targetBidder, targetDealId); + + final String rule = matchingStrategy.match(source, parameters); + return rule == null ? Collections.emptyList() : rules.get(rule); + } + + private PrebidConfigParameters createParameters(ImpMediaType mediaType, + String seat, + String bidder, + String dealId) { + + final List conditionsMatchers = List.of( + SimpleDirectParameter.of(mediaType.toString()), + StringUtils.isBlank(seat) + ? SimpleDirectParameter.of(bidder) + : SimpleDirectParameter.of(List.of(seat, bidder)), + StringUtils.isBlank(dealId) + ? PrebidConfigParameter.wildcard() + : SimpleDirectParameter.of(dealId)); + + return SimpleParameters.of(conditionsMatchers); + } +} diff --git a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java similarity index 94% rename from src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java rename to src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java index 77bbb7372ce..86dd863d1c3 100644 --- a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -32,6 +32,7 @@ public BigDecimal resolve(Set impMediaTypes, } final BigDecimal mediaTypeMinFactor = impMediaTypes.stream() + .map(type -> type == ImpMediaType.video_instream ? ImpMediaType.video : type) .map(adjustmentFactorsByMediaTypes::get) .map(bidderToFactor -> MapUtils.isNotEmpty(bidderToFactor) ? bidderToFactor.entrySet().stream() diff --git a/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java new file mode 100644 index 00000000000..cb4485bfa35 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java @@ -0,0 +1,89 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.BidderUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class FloorAdjustmentsResolver { + + private final BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + private final CurrencyConversionService currencyService; + + public FloorAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + this.bidAdjustmentsRulesResolver = Objects.requireNonNull(bidAdjustmentsRulesResolver); + this.currencyService = Objects.requireNonNull(currencyService); + } + + public Price resolve(Price initialBidFloorPrice, + BidRequest bidRequest, + Set targetMediaTypes, + String targetBidder) { + + final String currency = bidRequest.getCur().getFirst(); + Price minimalBidFloorPrice = null; + BigDecimal minimalPriceBidFloorValue = new BigDecimal(Integer.MAX_VALUE); + + for (ImpMediaType targetMediaType : targetMediaTypes) { + final Price resolvedPrice = resolve(initialBidFloorPrice, bidRequest, targetMediaType, targetBidder); + final BigDecimal convertedResolvedValue = currencyService.convertCurrency( + resolvedPrice.getValue(), bidRequest, resolvedPrice.getCurrency(), currency); + if (convertedResolvedValue.compareTo(minimalPriceBidFloorValue) < 0) { + minimalBidFloorPrice = resolvedPrice; + minimalPriceBidFloorValue = convertedResolvedValue; + } + } + + return ObjectUtils.firstNonNull(minimalBidFloorPrice, initialBidFloorPrice); + } + + public Price resolve(Price initialBidFloorPrice, + BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetBidder) { + + final List rules = bidAdjustmentsRulesResolver.resolve( + bidRequest, targetMediaType, targetBidder); + return reversePrice(initialBidFloorPrice, rules, bidRequest); + } + + private Price reversePrice(Price price, + List bidAdjustmentRules, + BidRequest bidRequest) { + + final List reversedRules = bidAdjustmentRules.reversed(); + final String resolvedCurrency = price.getCurrency(); + BigDecimal resolvedPrice = price.getValue(); + + for (BidAdjustmentsRule rule : reversedRules) { + final BidAdjustmentType adjustmentType = rule.getAdjType(); + final BigDecimal adjustmentValue = rule.getValue(); + final String adjustmentCurrency = rule.getCurrency(); + + switch (adjustmentType) { + case MULTIPLIER -> resolvedPrice = resolvedPrice.divide(adjustmentValue, 4, RoundingMode.HALF_EVEN); + case CPM -> { + final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency( + adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency); + resolvedPrice = BidderUtil.roundFloor(resolvedPrice.add(convertedAdjustmentValue)); + } + case STATIC -> throw new PreBidException("STATIC type can't be applied to a floor price"); + } + } + + return Price.of(resolvedCurrency, resolvedPrice); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java index 385a7644811..c57f5e7b26a 100644 --- a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java @@ -1,52 +1,15 @@ package org.prebid.server.bidadjustments.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; -import org.apache.commons.collections4.MapUtils; -import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; -import org.prebid.server.bidadjustments.BidAdjustmentsResolver; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @Value(staticConstructor = "of") public class BidAdjustments { - private static final String RULE_SCHEME = - "%s" + BidAdjustmentsResolver.DELIMITER + "%s" + BidAdjustmentsResolver.DELIMITER + "%s"; - - Map> rules; - - public static BidAdjustments of(ExtRequestBidAdjustments bidAdjustments) { - if (bidAdjustments == null) { - return BidAdjustments.of(Collections.emptyMap()); - } - - final Map> rules = new HashMap<>(); - - final Map>>> mediatypes = - bidAdjustments.getMediatype(); - - if (MapUtils.isEmpty(mediatypes)) { - return BidAdjustments.of(Collections.emptyMap()); - } - - for (String mediatype : mediatypes.keySet()) { - if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { - final Map>> bidders = mediatypes.get(mediatype); - for (String bidder : bidders.keySet()) { - final Map> deals = bidders.get(bidder); - for (String dealId : deals.keySet()) { - rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); - } - } - } - } - - return BidAdjustments.of(MapUtils.unmodifiableMap(rules)); - } + @JsonProperty("mediatype") + Map>>> rules; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java similarity index 71% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java rename to src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java index a857575a85f..dec501d71b2 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java @@ -1,15 +1,14 @@ -package org.prebid.server.proto.openrtb.ext.request; +package org.prebid.server.bidadjustments.model; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; -import org.prebid.server.bidadjustments.model.BidAdjustmentType; import java.math.BigDecimal; @Builder(toBuilder = true) @Value -public class ExtRequestBidAdjustmentsRule { +public class BidAdjustmentsRule { @JsonProperty("adjtype") BidAdjustmentType adjType; diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java new file mode 100644 index 00000000000..6d64ca1330e --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java @@ -0,0 +1,50 @@ +package org.prebid.server.bidadjustments.model; + +import lombok.Value; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.collections4.map.CaseInsensitiveMap; +import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class BidAdjustmentsRules { + + private static final String RULE_SCHEME = + "%s" + BidAdjustmentsRulesResolver.DELIMITER + "%s" + BidAdjustmentsRulesResolver.DELIMITER + "%s"; + + Map> rules; + + public static BidAdjustmentsRules of(BidAdjustments bidAdjustments) { + if (bidAdjustments == null) { + return BidAdjustmentsRules.of(Collections.emptyMap()); + } + + final Map> rules = new CaseInsensitiveMap<>(); + + final Map>>> mediatypes = + bidAdjustments.getRules(); + + if (MapUtils.isEmpty(mediatypes)) { + return BidAdjustmentsRules.of(Collections.emptyMap()); + } + + for (String mediatype : mediatypes.keySet()) { + if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + for (String dealId : deals.keySet()) { + rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); + } + } + } + } + + return BidAdjustmentsRules.of(MapUtils.unmodifiableMap(rules)); + } + +} diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java index 6238b17eec2..d943c6b92e9 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java @@ -3,8 +3,10 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.bidder.model.Price; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -22,6 +24,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -30,12 +33,15 @@ public class BasicPriceFloorAdjuster implements PriceFloorAdjuster { private static final int ADJUSTMENT_SCALE = 4; private static final BiFunction DIVIDE_FUNCTION = (priceFloor, factor) -> priceFloor.divide(factor, ADJUSTMENT_SCALE, RoundingMode.HALF_EVEN); - private static final BiFunction MULTIPLY_FUNCTION = BigDecimal::multiply; private final FloorAdjustmentFactorResolver floorAdjustmentFactorResolver; + private final FloorAdjustmentsResolver floorAdjustmentsResolver; + + public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver, + FloorAdjustmentsResolver floorAdjustmentsResolver) { - public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { this.floorAdjustmentFactorResolver = Objects.requireNonNull(floorAdjustmentFactorResolver); + this.floorAdjustmentsResolver = Objects.requireNonNull(floorAdjustmentsResolver); } @Override @@ -45,36 +51,46 @@ public Price adjustForImp(Imp imp, Account account, List debugWarnings) { - return adjust(imp, bidder, bidRequest, account, DIVIDE_FUNCTION); - } + final ExtRequestBidAdjustmentFactors bidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); + final BigDecimal impBidFloor = imp.getBidfloor(); - @Override - public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return adjust(imp, bidder, bidRequest, account, MULTIPLY_FUNCTION); - } + if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null) { + return Price.of(imp.getBidfloorcur(), impBidFloor); + } - private Price adjust(Imp imp, - String bidder, - BidRequest bidRequest, - Account account, - BiFunction function) { + final Set mediaTypes = retrieveImpMediaTypes(imp); + final Price adjustedBidFloor = adjustPrice(imp, bidder, impBidFloor, bidAdjustmentFactors, mediaTypes); - final ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); - final BigDecimal impBidFloor = imp.getBidfloor(); + try { + return floorAdjustmentsResolver.resolve(adjustedBidFloor, bidRequest, mediaTypes, bidder); + } catch (PreBidException e) { + return adjustedBidFloor; + } + } + + private Price adjustPrice(Imp imp, + String bidder, + BigDecimal impBidFloor, + ExtRequestBidAdjustmentFactors bidAdjustmentFactors, + Set mediaTypes) { - if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null || extractBidAdjustmentFactors == null) { + if (bidAdjustmentFactors == null) { return Price.of(imp.getBidfloorcur(), impBidFloor); } - final Set impMediaTypes = retrieveImpMediaTypes(imp); - final BigDecimal factor = floorAdjustmentFactorResolver.resolve( - impMediaTypes, extractBidAdjustmentFactors, bidder); - - final BigDecimal adjustedBidFloor = factor != null && factor.compareTo(BigDecimal.ONE) != 0 - ? BidderUtil.roundFloor(function.apply(impBidFloor, factor)) + final BigDecimal factor = floorAdjustmentFactorResolver.resolve(mediaTypes, bidAdjustmentFactors, bidder); + final BigDecimal adjustedBidFloorValue = factor != null && factor.compareTo(BigDecimal.ONE) != 0 + ? BidderUtil.roundFloor(DIVIDE_FUNCTION.apply(impBidFloor, factor)) : impBidFloor; - return Price.of(imp.getBidfloorcur(), adjustedBidFloor); + return Price.of(imp.getBidfloorcur(), adjustedBidFloorValue); + } + + private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustmentfactors) + .orElse(null); } private static boolean shouldAdjustBidFloor(BidRequest bidRequest, Account account) { @@ -95,7 +111,7 @@ private static Set retrieveImpMediaTypes(Imp imp) { if (imp.getVideo() != null) { final Integer placement = imp.getVideo().getPlacement(); if (placement == null || Objects.equals(placement, 1)) { - availableMediaTypes.add(ImpMediaType.video); + availableMediaTypes.add(ImpMediaType.video_instream); } else { availableMediaTypes.add(ImpMediaType.video_outstream); } @@ -126,11 +142,4 @@ private static Boolean shouldAdjustBidFloorByAccount(Account account) { return ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getAdjustForBidAdjustment); } - - private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extPrebid = ObjectUtil.getIfNotNull(extRequest, ExtRequest::getPrebid); - - return ObjectUtil.getIfNotNull(extPrebid, ExtRequestPrebid::getBidadjustmentfactors); - } } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 132bf86e782..1b9c7425385 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -1,10 +1,8 @@ package org.prebid.server.floors; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -36,7 +34,9 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; @@ -50,15 +50,12 @@ public class BasicPriceFloorEnforcer implements PriceFloorEnforcer { private static final int ENFORCE_RATE_MAX = 100; private final CurrencyConversionService currencyConversionService; - private final PriceFloorAdjuster priceFloorAdjuster; private final Metrics metrics; public BasicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, - PriceFloorAdjuster priceFloorAdjuster, Metrics metrics) { this.currencyConversionService = Objects.requireNonNull(currencyConversionService); - this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); this.metrics = Objects.requireNonNull(metrics); } @@ -165,9 +162,12 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, } final BigDecimal price = bid.getPrice(); + final Map originalPriceFloors = Optional.ofNullable(auctionParticipation.getBidderRequest()) + .map(BidderRequest::getOriginalPriceFloors) + .orElse(Collections.emptyMap()); + final BigDecimal floor = resolveFloor( - bidderResponse.getBidder(), - account, + originalPriceFloors, bidderBid, bidderBidRequest, bidRequest, @@ -211,8 +211,7 @@ private static boolean enforceDealFloors(AuctionParticipation auctionParticipati return BooleanUtils.isTrue(requestEnforceDealFloors) && BooleanUtils.isTrue(accountEnforceDealFloors); } - private BigDecimal resolveFloor(String bidder, - Account account, + private BigDecimal resolveFloor(Map originalPriceFloors, BidderBid bidderBid, BidRequest bidderBidRequest, BidRequest bidRequest, @@ -226,14 +225,15 @@ private BigDecimal resolveFloor(String bidder, return convertIfRequired(customBidderFloor, priceFloorInfo.getCurrency(), bidderBidRequest, bidRequest); } - final Imp imp = correspondingImp(bidderBid.getBid(), bidderBidRequest.getImp()); - final Price correctedImpFloor = priceFloorAdjuster.revertAdjustmentForImp(imp, bidder, bidRequest, account); final String bidRequestCurrency = resolveBidRequestCurrency(bidRequest); + final Price originalFloorPrice = originalPriceFloors.get(bidderBid.getBid().getImpid()); - return convertCurrency( - correctedImpFloor.getValue(), + return originalFloorPrice == null + ? null + : convertCurrency( + originalFloorPrice.getValue(), bidRequest, - correctedImpFloor.getCurrency(), + originalFloorPrice.getCurrency(), bidRequestCurrency); } catch (PreBidException e) { final String logMessage = "Price floors enforcement failed for request id: %s, reason: %s" @@ -286,15 +286,6 @@ private static String resolveBidRequestCurrency(BidRequest bidRequest) { return CollectionUtils.isEmpty(currencies) ? null : currencies.getFirst(); } - private static Imp correspondingImp(Bid bid, List imps) { - final String impId = bid.getImpid(); - return ListUtils.emptyIfNull(imps).stream() - .filter(imp -> Objects.equals(impId, imp.getId())) - .findFirst() - // Should never happen, see ResponseBidValidator usage. - .orElseThrow(() -> new PreBidException("Bid with impId %s doesn't have matched imp".formatted(impId))); - } - private static boolean isPriceBelowFloor(BigDecimal price, BigDecimal bidFloor) { return bidFloor != null && price.compareTo(bidFloor) < 0; } diff --git a/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java index ba36e705ba1..b1273311049 100644 --- a/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java @@ -67,11 +67,6 @@ public Price adjustForImp(Imp imp, .orElseGet(() -> delegate.adjustForImp(imp, bidder, bidRequest, account, debugWarnings)); } - @Override - public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return delegate.revertAdjustmentForImp(imp, bidder, bidRequest, account); - } - private static boolean isNoSignalBidder(String bidder, List noSignalBidders) { return noSignalBidders.stream().anyMatch(noSignalBidder -> StringUtils.equalsIgnoreCase(noSignalBidder, bidder)) || noSignalBidders.contains(ALL_BIDDERS); diff --git a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java index 89a2ac7bc24..0654e6b523f 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java @@ -12,8 +12,6 @@ public interface PriceFloorAdjuster { Price adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account, List debugWarnings); - Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account); - static NoOpPriceFloorAdjuster noOp() { return new NoOpPriceFloorAdjuster(); } @@ -29,10 +27,5 @@ public Price adjustForImp(Imp imp, return ObjectUtil.getIfNotNull(imp, i -> Price.of(i.getBidfloorcur(), i.getBidfloor())); } - - @Override - public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return ObjectUtil.getIfNotNull(imp, i -> Price.of(i.getBidfloorcur(), i.getBidfloor())); - } } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java deleted file mode 100644 index ab0565ce44e..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request; - -import lombok.Builder; -import lombok.Value; - -import java.util.List; -import java.util.Map; - -@Builder(toBuilder = true) -@Value -public class ExtRequestBidAdjustments { - - Map>>> mediatype; - -} diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java index 16a79d6c0f6..6da6838a65d 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -1,7 +1,9 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.BasicPriceFloorAdjuster; @@ -53,10 +55,8 @@ PriceFloorFetcher priceFloorFetcher( @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") - PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, - PriceFloorAdjuster priceFloorAdjuster, - Metrics metrics) { - return new BasicPriceFloorEnforcer(currencyConversionService, priceFloorAdjuster, metrics); + PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, Metrics metrics) { + return new BasicPriceFloorEnforcer(currencyConversionService, metrics); } @Bean @@ -106,8 +106,18 @@ FloorAdjustmentFactorResolver floorsAdjustmentFactorResolver() { @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") - BasicPriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { - return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver); + FloorAdjustmentsResolver floorAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + return new FloorAdjustmentsResolver(bidAdjustmentsRulesResolver, currencyService); + } + + @Bean + @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") + BasicPriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver, + FloorAdjustmentsResolver floorAdjustmentsResolver) { + + return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver, floorAdjustmentsResolver); } @Bean 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 1e496ea7e9a..360d77b2bae 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -34,7 +34,7 @@ import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.WinningBidComparatorFactory; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentFactorResolver; import org.prebid.server.auction.categorymapping.BasicCategoryMappingService; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.categorymapping.NoOpCategoryMappingService; @@ -66,7 +66,8 @@ import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory; import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; import org.prebid.server.bidadjustments.BidAdjustmentsResolver; -import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.BidderErrorNotifier; @@ -431,7 +432,7 @@ AuctionRequestFactory auctionRequestFactory( DebugResolver debugResolver, JacksonMapper mapper, GeoLocationServiceWrapper geoLocationServiceWrapper, - BidAdjustmentsRetriever bidAdjustmentsRetriever) { + BidAdjustmentsEnricher bidAdjustmentsEnricher) { return new AuctionRequestFactory( maxRequestSize, @@ -448,7 +449,7 @@ AuctionRequestFactory auctionRequestFactory( debugResolver, mapper, geoLocationServiceWrapper, - bidAdjustmentsRetriever); + bidAdjustmentsEnricher); } @Bean @@ -907,7 +908,8 @@ ExchangeService exchangeService( metrics, clock, mapper, - criteriaLogManager, enabledStrictAppSiteDoohValidation); + criteriaLogManager, + enabledStrictAppSiteDoohValidation); } @Bean @@ -1190,13 +1192,20 @@ SkippedAuctionService skipAuctionService(StoredResponseProcessor storedResponseP } @Bean - BidAdjustmentsRetriever bidAdjustmentsRetriever(JacksonMapper mapper, JsonMerger jsonMerger) { - return new BidAdjustmentsRetriever(mapper, jsonMerger, logSamplingRate); + BidAdjustmentsEnricher bidAdjustmentsEnricher(JacksonMapper mapper, JsonMerger jsonMerger) { + return new BidAdjustmentsEnricher(mapper, jsonMerger, logSamplingRate); } @Bean - BidAdjustmentsResolver bidAdjustmentsResolver(CurrencyConversionService currencyService) { - return new BidAdjustmentsResolver(currencyService); + BidAdjustmentsResolver bidAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + return new BidAdjustmentsResolver(currencyService, bidAdjustmentsRulesResolver); + } + + @Bean + BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver(JacksonMapper mapper) { + return new BidAdjustmentsRulesResolver(mapper); } @Bean diff --git a/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java b/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java index ea8a841df95..9cbcae877da 100644 --- a/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java +++ b/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java @@ -14,10 +14,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.TreeSet; /** * Priority order for four column rule sets: @@ -174,7 +174,7 @@ private static List generateWildcardsIndices(Iterable toSet(Iterable iterable) { - return iterable instanceof Set set ? set : fill(new HashSet<>(), iterable); + return fill(new TreeSet<>(String.CASE_INSENSITIVE_ORDER), iterable); } private static > C fill(C destination, Iterable source) { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy index 4fcfc1125e1..92af741601f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy @@ -1,16 +1,19 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class BidAdjustmentRule { @JsonProperty('*') Map> wildcardBidder Map> generic + Map> openx Map> alias + @JsonProperty("ALIAS") + Map> aliasUpperCase + @JsonProperty("AlIaS") + Map> aliasCamelCase + Map> amx } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index aa9da45a4b6..26e9ddc2057 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.response.auction.MediaType import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP @@ -57,6 +58,10 @@ class BidRequest { getDefaultRequest(channel, Imp.getDefaultImpression(AUDIO)) } + static BidRequest getDefaultBidRequest(MediaType mediaType, DistributionChannel channel = SITE) { + getDefaultRequest(channel, Imp.getDefaultImpression(mediaType)) + } + static BidRequest getDefaultStoredRequest() { getDefaultBidRequest().tap { site = null diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index 127ed32bfd9..353f5516935 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -8,6 +8,8 @@ import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils +import static groovy.lang.Closure.DELEGATE_FIRST + @ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode class Bid implements ObjectMapperWrapper { @@ -70,6 +72,23 @@ class Bid implements ObjectMapperWrapper { } } + static List getDefaultMultyTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { + List bids = [] + if (imp.banner) bids << createBid(imp, BidMediaType.BANNER) { adm = null } + if (imp.video) bids << createBid(imp, BidMediaType.VIDEO) + if (imp.nativeObj) bids << createBid(imp, BidMediaType.NATIVE) + if (imp.audio) bids << createBid(imp, BidMediaType.AUDIO) { adm = null } + + if (commonInit) { + bids.each { bid -> + commonInit.delegate = bid + commonInit.resolveStrategy = DELEGATE_FIRST + commonInit() + } + } + bids + } + void setAdm(Object adm) { if (adm instanceof Adm) { this.adm = encode(adm) @@ -79,4 +98,15 @@ class Bid implements ObjectMapperWrapper { this.adm = null } } + + private static Bid createBid(Imp imp, BidMediaType type, @DelegatesTo(Bid) Closure init = null) { + def bid = getDefaultBid(imp) + bid.mediaType = type + if (init) { + init.delegate = bid + init.resolveStrategy = DELEGATE_FIRST + init() + } + bid + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy index 76aa2a558f9..84b5a5d9953 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType enum BidMediaType { @@ -15,4 +16,16 @@ enum BidMediaType { BidMediaType(Integer value) { this.value = value } + + static BidMediaType from(BidAdjustmentMediaType mediaType) { + return switch (mediaType) { + case BidAdjustmentMediaType.BANNER -> BANNER + case BidAdjustmentMediaType.VIDEO -> VIDEO + case BidAdjustmentMediaType.VIDEO_IN_STREAM -> VIDEO + case BidAdjustmentMediaType.VIDEO_OUT_STREAM -> VIDEO + case BidAdjustmentMediaType.AUDIO -> AUDIO + case BidAdjustmentMediaType.NATIVE -> NATIVE + default -> throw new IllegalArgumentException("Unknown media type: " + mediaType); + }; + } } diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index 31df1efc8d5..b9c173baa54 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -30,6 +30,7 @@ import org.prebid.server.functional.model.response.setuid.SetuidResponse import org.prebid.server.functional.model.response.status.StatusResponse import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.ObjectMapperWrapper +import org.prebid.server.functional.util.PBSUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -412,7 +413,12 @@ class PrebidServerService implements ObjectMapperWrapper { } Boolean isContainLogsByValue(String value) { - getPbsLogsByValue(value) != null + try { + PBSUtils.waitUntil({ getPbsLogsByValue(value) != null }) + true + } catch (IllegalStateException ignored) { + false + } } private String getPbsLogsByValue(String value) { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 91ec927b1c2..052bcf2f69f 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -6,6 +6,7 @@ import org.testcontainers.containers.PostgreSQLContainer import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.testcontainers.container.PrebidServerContainer.ADMIN_ENDPOINT_PASSWORD import static org.prebid.server.functional.testcontainers.container.PrebidServerContainer.ADMIN_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY final class PbsConfig { @@ -29,7 +30,7 @@ LIMIT 1 static final Map DEFAULT_ENV = [ "logging.sampling-rate" : "1.0", - "auction.ad-server-currency" : "USD", + "auction.ad-server-currency" : DEFAULT_CURRENCY.value, "auction.stored-requests-timeout-ms" : "1000", "metrics.prefix" : "prebid", "status-response" : "ok", @@ -136,5 +137,13 @@ LIMIT 1 "adapters.generic.aliases.adrino.meta-info.site-media-types" : ""] } + static Map getCurrencyConverterConfig() { + ["auction.ad-server-currency" : DEFAULT_CURRENCY.value, + "currency-converter.external-rates.enabled" : "true", + "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), + "currency-converter.external-rates.default-timeout-ms": "4000", + "currency-converter.external-rates.refresh-period-ms" : "900000"] + } + private PbsConfig() {} } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy index 6f5b74bda61..6246f8c9f4d 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy @@ -7,16 +7,18 @@ import org.testcontainers.containers.MockServerContainer import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY_RATES class CurrencyConversion extends NetworkScaffolding { static final String CURRENCY_ENDPOINT_PATH = "/currency" + private static final CurrencyConversionRatesResponse DEFAULT_RATES_RESPONSE = CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES) CurrencyConversion(MockServerContainer mockServerContainer) { super(mockServerContainer, CURRENCY_ENDPOINT_PATH) } - void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse) { + void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse = DEFAULT_RATES_RESPONSE) { setResponse(request, conversionRatesResponse) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index 2943c78201a..13479030d1d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -2,6 +2,8 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.bidderspecific.BidderRequest import org.prebid.server.functional.model.response.amp.AmpResponse +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidMediaType import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.BidderCall import org.prebid.server.functional.repository.HibernateRepositoryService @@ -103,6 +105,10 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { } } + protected static List getMediaTypedBids(BidResponse bidResponse, BidMediaType mediaType) { + bidResponse.seatbid*.bid.collectMany { it }.findAll { it.mediaType == mediaType } + } + protected static Map> getRequests(AmpResponse ampResponse) { ampResponse.ext.debug.bidders.collectEntries { bidderName, bidderCalls -> collectRequestByBidderName(bidderName, bidderCalls) diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 8effa51b231..7da1c89fffb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -1,13 +1,12 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.Currency + import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AlternateBidderCodes import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.Account -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.request.auction.AdjustmentRule import org.prebid.server.functional.model.request.auction.AdjustmentType import org.prebid.server.functional.model.request.auction.Amx @@ -22,16 +21,16 @@ import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils -import java.math.RoundingMode -import java.time.Instant - import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.GBP import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.ACUITYADS import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.AMX import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS @@ -62,22 +61,23 @@ class BidAdjustmentSpec extends BaseSpec { private static final BigDecimal MAX_MULTIPLIER_ADJUST_VALUE = 99 private static final BigDecimal MAX_CPM_ADJUST_VALUE = Integer.MAX_VALUE private static final BigDecimal MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE - private static final Currency DEFAULT_CURRENCY = USD private static final int BID_ADJUST_PRECISION = 4 - private static final int PRICE_PRECISION = 3 private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, - (GBP): 0.793776804452961], - (GBP): [(USD): 1.2597999770088517, - (EUR): 1.1495574203931487], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) - } + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) private static final Map AMX_CONFIG = ["adapters.amx.enabled" : "true", "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] - private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig + AMX_CONFIG) + private static PrebidServerService pbsService + + def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() + pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig + AMX_CONFIG) + } + + @Override + def cleanupSpec() { + pbsServiceFactory.removeContainer(PbsConfig.currencyConverterConfig + AMX_CONFIG) + } def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment" @@ -201,9 +201,12 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should adjust bid price for matching bidder when request has bidAdjustments config"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency and: "Default bid response" def originalPrice = PBSUtils.randomPrice @@ -226,9 +229,92 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain default currency" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder and left original bidderRequest with null floors when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = null + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [null] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -279,10 +365,18 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def dealId = PBSUtils.randomString def currency = USD + def firstImpPrice = PBSUtils.randomPrice + def secondImpPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) - bidRequest.imp.add(Imp.defaultImpression) bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) and: "Default bid response" def originalPrice = PBSUtils.randomPrice @@ -297,7 +391,6 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted for big with dealId" - response.seatbid.first.bid.find { it.dealid == dealId } assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] and: "Price shouldn't be updated for bid with different dealId" @@ -311,9 +404,11 @@ class BidAdjustmentSpec extends BaseSpec { assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [firstImpPrice, secondImpPrice].sort() where: adjustmentType | ruleValue | mediaType | bidRequest @@ -361,9 +456,14 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS should adjust bid price for matching bidder when account config has bidAdjustments"() { - given: "Default bid response" - def originalPrice = PBSUtils.randomPrice + given: "BidRequest with floors" + def impPrice = PBSUtils.randomPrice def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = currency seatbid.first.bid.first.price = originalPrice @@ -389,9 +489,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -439,8 +541,13 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { - given: "Default BidRequest with ext.prebid.bidAdjustments" + given: "BidRequest with floors" + def impPrice = PBSUtils.randomPrice def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default BidRequest with ext.prebid.bidAdjustments" def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) bidRequest.cur = [currency] @@ -472,9 +579,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -524,11 +633,14 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should prioritize exact bid price adjustment for matching bidder when request has exact and general bidAdjustment"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def exactRulePrice = PBSUtils.randomPrice + def impPrice = PBSUtils.randomPrice def currency = USD def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) } @@ -553,19 +665,24 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] } def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: PBSUtils.randomPrice, currency: currency) def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: PBSUtils.randomPrice, currency: currency) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) } @@ -592,9 +709,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: firstRuleType | secondRuleType @@ -613,8 +732,12 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.randomPrice, currency: GBP) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.randomPrice def bidRequest = BidRequest.defaultBidRequest.tap { cur = [EUR] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) } @@ -630,9 +753,9 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - def convertedAdjustment = convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) + def convertedAdjustment = CurrencyUtil.convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) - assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, bidRequest.cur.first) + assert response.seatbid.first.bid.first.price == CurrencyUtil.convertCurrency(adjustedBidPrice, bidResponse.cur, currency) and: "Original bid price and currency should be presented in bid.ext" verifyAll(response.seatbid.first.bid.first.ext) { @@ -640,21 +763,27 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert bidderRequest.cur == bidRequest.cur + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] } def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: GBP) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.randomPrice def bidRequest = BidRequest.defaultBidRequest.tap { - cur = [EUR] + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) } - and: "Default bid response with JPY currency" + and: "Default bid response with USD currency" def originalPrice = PBSUtils.randomPrice def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = USD @@ -666,7 +795,7 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted and converted to original request cur" - assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidRequest.cur.first) + assert response.seatbid.first.bid.first.price == CurrencyUtil.convertCurrency(adjustmentRule.value, adjustmentRule.currency, currency) assert response.cur == bidRequest.cur.first and: "Original bid price and currency should be presented in bid.ext" @@ -675,19 +804,24 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert bidderRequest.cur == bidRequest.cur + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] } def "PBS should apply bidAdjustments after bidAdjustmentFactors when both are present"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def bidAdjustmentFactorsPrice = PBSUtils.randomPrice def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) } @@ -714,23 +848,25 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [MULTIPLIER, CPM, STATIC] } def "PBS shouldn't adjust bid price for matching bidder when request has invalid value bidAdjustments config"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency and: "Default bid response" def originalPrice = PBSUtils.randomPrice @@ -760,12 +896,13 @@ class BidAdjustmentSpec extends BaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -854,9 +991,12 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS shouldn't adjust bid price for matching bidder when request has different bidder name in bidAdjustments config"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -884,24 +1024,26 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [MULTIPLIER, CPM, STATIC] } def "PBS shouldn't adjust bid price for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def adjustmentPrice = PBSUtils.randomPrice.toDouble() def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -933,12 +1075,13 @@ class BidAdjustmentSpec extends BaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [CPM, STATIC] @@ -948,9 +1091,12 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def adjustmentPrice = PBSUtils.randomPrice def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) } @@ -978,24 +1124,26 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [MULTIPLIER, CPM, STATIC] } def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown adjustmentType"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def adjustmentPrice = PBSUtils.randomPrice.toDouble() def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -1027,8 +1175,7 @@ class BidAdjustmentSpec extends BaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain currency from request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -1039,9 +1186,14 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def adjustmentPrice = PBSUtils.randomPrice + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.tap { + bidFloor = impPrice + bidFloorCur = currency + } ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -1075,9 +1227,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [CPM, STATIC] @@ -1193,43 +1347,323 @@ class BidAdjustmentSpec extends BaseSpec { bidAdjustmentFactor << [0.9, 1.1] } - private static Map getExternalCurrencyConverterConfig() { - ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] + def "PBS shouldn't adjust bid price when bid adjustment rule doesn't match with bidder code"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def bidAdjustmentRule = new BidAdjustmentRule((bidAdjustmentRuleBidder): [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): bidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ACUITYADS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ACUITYADS + + where: + bidAdjustmentRuleBidder << ["alias", "aliasUpperCase", "aliasCamelCase"] } - private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { - return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) + def "PBS should adjust bid price when two bid adjustment rules are compatible"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def dealId = PBSUtils.randomString + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def firstBidAdjustmentRule = new BidAdjustmentRule(amx: [(dealId): [adjustmentRule]]) + def secondBidAdjustmentRule = new BidAdjustmentRule(amx: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): firstBidAdjustmentRule, + (ANY) : secondBidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code and dealId" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: AMX) + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + } + + def "PBS should adjust bid price when bid adjustment bidder and bidder code different"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def bidAdjustmentRule = new BidAdjustmentRule((bidAdjustmentRuleBidder): [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): bidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + + where: + bidAdjustmentRuleBidder << ["alias", "aliasUpperCase", "aliasCamelCase"] } - private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { - def conversionRate - if (fromCurrency == toCurrency) { - conversionRate = 1 - } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { - conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { - conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] - } else { - conversionRate = getCrossConversionRate(fromCurrency, toCurrency) - } - conversionRate + def "PBS should adjust bid price when bid adjustment bidder and bidder code same as requested"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(amx: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX } - private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { - for (Map rates : DEFAULT_CURRENCY_RATES.values()) { - def fromRate = rates?[fromCurrency] - def toRate = rates?[toCurrency] + def "PBS should adjust bid price when bid adjustment bidder is the same as bidder code"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + } - if (fromRate && toRate) { - return toRate / fromRate + def "PBS should adjust bid price when bid adjustment wildcard bidder and bidder code specified"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(wildcardBidder: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } } } - null + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS } private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, @@ -1248,19 +1682,11 @@ class BidAdjustmentSpec extends BaseSpec { } private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { - BidRequest.defaultVideoRequest.tap { - imp.first.video.tap { - placement = videoPlacementSubtypes - } - } + getDefaultVideoRequestWithPlcmtAndPlacement(null, videoPlacementSubtypes) } private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { - BidRequest.defaultVideoRequest.tap { - imp.first.video.tap { - plcmt = videoPlcmtSubtype - } - } + getDefaultVideoRequestWithPlcmtAndPlacement(videoPlcmtSubtype, null) } private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, diff --git a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy index df5bba70028..662f9423848 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -1,13 +1,11 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion - -import java.math.RoundingMode +import org.prebid.server.functional.util.CurrencyUtil import static org.prebid.server.functional.model.Currency.CAD import static org.prebid.server.functional.model.Currency.CHF @@ -16,21 +14,17 @@ import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY class CurrencySpec extends BaseSpec { - private static final Currency DEFAULT_CURRENCY = USD - private static final int PRICE_PRECISION = 3 - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(USD): 1, - (EUR): 0.9249838127832763, - (CHF): 0.9033391915641477, - (JPY): 151.1886041994265, - (CAD): 1.357136250115623], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) + private static PrebidServerService pbsService + + def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() + pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig) } - private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig) def "PBS should return currency rates"() { when: "PBS processes bidders params request" @@ -85,7 +79,7 @@ class CurrencySpec extends BaseSpec { then: "Auction response should contain bid in #requestCurrency currency" assert bidResponse.cur == requestCurrency def bidPrice = bidResponse.seatbid[0].bid[0].price - assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidPrice == CurrencyUtil.convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency @@ -109,7 +103,7 @@ class CurrencySpec extends BaseSpec { then: "Auction response should contain bid in #requestCurrency currency" assert bidResponse.cur == requestCurrency def bidPrice = bidResponse.seatbid[0].bid[0].price - assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidPrice == CurrencyUtil.convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency @@ -133,7 +127,7 @@ class CurrencySpec extends BaseSpec { then: "Auction response should contain bid in #requestCurrency currency" assert bidResponse.cur == requestCurrency def bidPrice = bidResponse.seatbid[0].bid[0].price - assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidPrice == CurrencyUtil.convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency @@ -189,43 +183,4 @@ class CurrencySpec extends BaseSpec { and: "Bid response shouldn't contain warnings" assert !bidResponse.ext.warnings } - - private static Map getExternalCurrencyConverterConfig() { - ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] - } - - private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { - return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) - } - - private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { - def conversionRate - if (fromCurrency == toCurrency) { - conversionRate = 1 - } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { - conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { - conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] - } else { - conversionRate = getCrossConversionRate(fromCurrency, toCurrency) - } - conversionRate - } - - private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { - for (Map rates : DEFAULT_CURRENCY_RATES.values()) { - def fromRate = rates?[fromCurrency] - def toRate = rates?[toCurrency] - - if (fromRate && toRate) { - return toRate / fromRate - } - } - - null - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy index 767c4b8e544..0b6f62923c7 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy @@ -372,10 +372,14 @@ class StoredResponseSpec extends BaseSpec { } } - private static final List convertToComparableSeatBid(List seatBid) { - seatBid*.tap { - it.bid*.ext = null - it.group = null + private static List convertToComparableSeatBid(List seatBids) { + seatBids*.tap { seatBid -> + seatBid.bid*.tap { bid -> + bid.ext = null + bid.price = bid.price.setScale(3) + } + seatBid.group = null } + seatBids } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy new file mode 100644 index 00000000000..a1fa69e487e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy @@ -0,0 +1,1240 @@ +package org.prebid.server.functional.tests.pricefloors + +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.db.Account +import org.prebid.server.functional.model.pricefloors.PriceFloorField +import org.prebid.server.functional.model.pricefloors.Rule +import org.prebid.server.functional.model.request.auction.AdjustmentRule +import org.prebid.server.functional.model.request.auction.AdjustmentType +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidAdjustment +import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors +import org.prebid.server.functional.model.request.auction.BidAdjustmentRule +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.ExtPrebidFloors +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.MultiBid +import org.prebid.server.functional.model.request.auction.Native +import org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes +import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidMediaType +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.util.CurrencyUtil +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.request.auction.AdjustmentType.CPM +import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER +import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { + + private static final Integer MIN_ADJUST_VALUE = 0 + private static final Integer MAX_MULTIPLIER_ADJUST_VALUE = 99 + private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) + private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) + private static final Integer MAX_CPM_ADJUST_VALUE = 5 + private static final Integer MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE + private static final String WILDCARD = '*' + + private static final Map PBS_CONFIG = PbsConfig.currencyConverterConfig + + FLOORS_CONFIG + + GENERIC_ALIAS_CONFIG + + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + + ["adapter-defaults.ortb.multiformat-supported": "true"] + private static PrebidServerService pbsService + + def setupSpec() { + pbsService = pbsServiceFactory.getService(PBS_CONFIG) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG) + } + + def "PBS should reverse imp.floors for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.getRandomDecimal(impPrice) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain reversed floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should left original bidderRequest with null floors when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = null + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [null] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should reverse imp.floors for matching bidder when request with multiple imps has specific bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def firstImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def secondImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.impid == bidRequest.imp.first.id }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.impid == bidRequest.imp.last.id }.price == [bidResponse.seatbid.first.bid.last.price] + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain reversed imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [getReverseAdjustedPrice(firstImpPrice, ruleValue as BigDecimal, adjustmentType), secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder with specific dealId when request with multiple imps has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def dealId = PBSUtils.randomString + def currency = USD + def firstImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def secondImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [firstImpPrice, secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should reverse imp.floors for matching bidder when account config has bidAdjustments"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain reversed imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Account in the DB with bidAdjustments" + def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to request config" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should prioritize exact imp.floors reverser for matching bidder when request has exact and general bidAdjustment"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def exactRulePrice = PBSUtils.randomDecimal + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, exactRulePrice, STATIC)] + } + + def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: firstRuleValue, currency: currency) + def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: secondRuleValue, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType) + def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType) + assert response.seatbid.first.bid.first.price == adjustedBidPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [applyReverseAdjustments(impPrice, [firstRule, secondRule])] + + where: + firstRuleType | secondRuleType | firstRuleValue | secondRuleValue + MULTIPLIER | CPM | PBSUtils.randomPrice | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + MULTIPLIER | STATIC | PBSUtils.randomPrice | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + MULTIPLIER | MULTIPLIER | PBSUtils.randomPrice | PBSUtils.randomPrice + CPM | CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, 1) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, 1) + CPM | STATIC | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + CPM | MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.randomPrice + STATIC | CPM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.randomPrice + STATIC | STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + STATIC | MULTIPLIER | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.randomPrice + } + + def "PBS should prioritize revert with lower resulting value for matching bidder when request has multiple media types"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def impPrice = PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + def currency = USD + def firstRule = new BidAdjustmentRule(openx: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: firstRulePrice, currency: currency)]]) + def secondRule = new BidAdjustmentRule(openx: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: secondRulePrice, currency: currency)]]) + def bidRequest = getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM).tap { + cur = [currency] + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + imp[0].ext.prebid.bidder.generic = null + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + imp.first.banner = Banner.getDefaultBanner() + imp.first.nativeObj = Native.getDefaultNative() + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(primaryType): firstRule, (BANNER): secondRule]) + ext.prebid.multibid = [new MultiBid(bidder: OPENX, maxBids: 3)] + } + + and: "Default bid response" + def originalPrice = PBSUtils.getRandomDecimal() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid = Bid.getDefaultMultyTypesBids(bidRequest.imp.first) { + price = originalPrice + ext = new BidExt() + } + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to first matched rule" + getMediaTypedBids(response, BidMediaType.from(primaryType)).price == [getAdjustedPrice(originalPrice, firstRulePrice, MULTIPLIER)] + getMediaTypedBids(response, BidMediaType.BANNER).price == [getAdjustedPrice(originalPrice, secondRulePrice, MULTIPLIER)] + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain revert imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, [firstRulePrice, secondRulePrice].max(), MULTIPLIER)] + + where: + primaryType | firstRulePrice | secondRulePrice + VIDEO_IN_STREAM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + NATIVE | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + + VIDEO_IN_STREAM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + NATIVE | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + } + + def "PBS should convert CPM currency before adjustment when it different from original response currency"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE), currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Get currency rates" + def currencyRatesResponse = pbsService.sendCurrencyRatesRequest() + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def convertedAdjustment = CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur, currencyRatesResponse) + def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) + assert response.seatbid.first.bid.first.price == CurrencyUtil.getPriceAfterCurrencyConversion(adjustedBidPrice, bidResponse.cur, currency, currencyRatesResponse) + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + def convertedReverseAdjustment = CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + def reversedAdjustBidPrice = getReverseAdjustedPrice(impPrice, convertedReverseAdjustment, adjustmentRule.adjustmentType) + assert bidderRequest.imp.bidFloor == [reversedAdjustBidPrice] + } + + def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomDecimal, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response with USD currency" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Get currency rates" + def currencyRatesResponse = pbsService.sendCurrencyRatesRequest() + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted and converted to original request cur" + assert response.seatbid.first.bid.first.price == + CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + assert response.cur == bidRequest.cur.first + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + } + + def "PBS should apply bidAdjustments revert for imp.floors after bidAdjustmentFactors when both are present"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def bidAdjustmentFactorsPrice = PBSUtils.randomPrice + def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentValue, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice + assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + def reversedBidPrice = impPrice / bidAdjustmentFactorsPrice + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(reversedBidPrice, adjustmentRule.value, adjustmentType)] + + where: + adjustmentType | impPrice | adjustmentValue + MULTIPLIER | PBSUtils.getRandomPrice() | PBSUtils.getRandomPrice() + CPM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + STATIC | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder when request has invalid value bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + assert pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + + CPM | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + + STATIC | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder when request has different bidder name in bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown mediatype"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentPrice = PBSUtils.randomPrice + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown adjustmentType"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = impPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " + + "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when multiplier bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.tap { + bidFloor = impPrice + bidFloorCur = currency + } + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, adjustmentPrice, MULTIPLIER)] + + where: + adjustmentType << [CPM, STATIC] + } + + private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { + getDefaultVideoRequestWithPlcmtAndPlacement(null, videoPlacementSubtypes) + } + + private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { + getDefaultVideoRequestWithPlcmtAndPlacement(videoPlcmtSubtype, null) + } + + private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, + VideoPlacementSubtypes videoPlacementSubtypes) { + getBidRequestWithFloors(MediaType.VIDEO).tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + placement = videoPlacementSubtypes + } + } + } + + private static BigDecimal getReverseAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice / adjustedValue, FLOOR_VALUE_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice + adjustedValue, FLOOR_VALUE_PRECISION) + case STATIC: + return PBSUtils.roundDecimal(originalPrice, FLOOR_VALUE_PRECISION) + default: + return adjustedValue + } + } + + private static BigDecimal applyReverseAdjustments(BigDecimal originalPrice, List rules) { + if (!rules || rules.any { it.adjustmentType == STATIC }) { + return originalPrice + } + def result = originalPrice + rules.reverseEach { + result = getReverseAdjustedPrice(result, it.value, it.adjustmentType) + } + result + } + + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice * adjustedValue, FLOOR_VALUE_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice - adjustedValue, FLOOR_VALUE_PRECISION) + case STATIC: + return adjustedValue + default: + return originalPrice + } + } + + private static BidRequest getBidRequestWithFloors(MediaType type, + DistributionChannel channel = SITE) { + def floors = ExtPrebidFloors.extPrebidFloors.tap { + data.modelGroups.first.values = [(new Rule(channel: PBSUtils.randomString) + .getRule([PriceFloorField.CHANNEL])): PBSUtils.randomFloorValue] + } + BidRequest.getDefaultBidRequest(type, channel).tap { + ext.prebid.floors = floors + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index a604264b264..95ef1318637 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -1,6 +1,5 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig @@ -18,8 +17,8 @@ import org.prebid.server.functional.model.request.auction.ExtPrebidFloors import org.prebid.server.functional.model.request.auction.FetchStatus import org.prebid.server.functional.model.request.auction.Prebid import org.prebid.server.functional.model.request.auction.Video -import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.testcontainers.scaffolding.FloorsProvider import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils @@ -59,13 +58,15 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { "Price floors processing failed: $reason. Following parsing of request price floors is failed: $details" } + protected static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) + + protected static final int FLOOR_VALUE_PRECISION = 4 private static final int DEFAULT_MODEL_WEIGHT = 1 - private static final int CURRENCY_CONVERSION_PRECISION = 3 - private static final int FLOOR_VALUE_PRECISION = 4 protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG) def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() floorsProvider.setResponse() } @@ -119,11 +120,6 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { PBSUtils.getRandomNumber(DEFAULT_MODEL_WEIGHT, MAX_MODEL_WEIGHT) } - static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { - def adjustedValue = floorValue / bidAdjustment - PBSUtils.roundDecimal(adjustedValue, FLOOR_VALUE_PRECISION) - } - static BidRequest getBidRequestWithMultipleMediaTypes() { BidRequest.defaultBidRequest.tap { imp[0].video = Video.defaultVideo } } @@ -158,12 +154,4 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { protected BigDecimal getRoundedFloorValue(BigDecimal floorValue) { floorValue.setScale(FLOOR_VALUE_PRECISION, RoundingMode.HALF_EVEN) } - - protected BigDecimal getPriceAfterCurrencyConversion(BigDecimal value, - Currency currencyFrom, Currency currencyTo, - CurrencyRatesResponse currencyRatesResponse) { - def currencyRate = currencyRatesResponse.rates[currencyFrom.value][currencyTo.value] - def convertedValue = value * currencyRate - convertedValue.setScale(CURRENCY_CONVERSION_PRECISION, RoundingMode.HALF_EVEN) - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index 69d8dce297d..b569514d0c4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -1,16 +1,15 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.config.AccountPriceFloorsConfig import org.prebid.server.functional.model.config.PriceFloorsFetch -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.auction.ImpExtPrebidFloors import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.BOGUS @@ -22,26 +21,20 @@ import static org.prebid.server.functional.model.request.auction.FetchStatus.NON import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID -import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, - (GBP): 0.793776804452961], - (GBP): [(USD): 1.2597999770088517, - (EUR): 1.1495574203931487], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) - } private static final String GENERAL_ERROR_METRIC = "price-floors.general.err" - private static final Map CURRENCY_CONVERTER_CONFIG = ["auction.ad-server-currency" : "USD", - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] - private final PrebidServerService currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + - CURRENCY_CONVERTER_CONFIG) + + private static PrebidServerService currencyFloorsPbsService + + def setupSpec() { + currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + PbsConfig.currencyConverterConfig) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(FLOORS_CONFIG + PbsConfig.currencyConverterConfig) + } def "PBS should update bidFloor, bidFloorCur for signalling when request.cur is specified"() { given: "Default BidRequest with cur" @@ -53,6 +46,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -100,7 +97,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() and: "Bid response with 2 bids: price < floorMin, price = floorMin" - def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorValue, + def convertedMinFloorValue = CurrencyUtil.getPriceAfterCurrencyConversion(floorValue, floorsResponse.modelGroups[0].currency, bidRequest.cur[0], currencyRatesResponse) def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = EUR @@ -137,9 +134,13 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Get currency rates" def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur, floorValur lower then floorMin" def floorProviderCur = EUR - def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorMin, + def convertedMinFloorValue = CurrencyUtil.getPriceAfterCurrencyConversion(floorMin, bidRequest.ext.prebid.floors.floorMinCur, floorProviderCur, currencyRatesResponse) def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -184,6 +185,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = EUR def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -247,6 +252,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -283,6 +292,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -323,7 +336,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Bid response with 2 bids: price < floorMin, price = floorMin" def bidResponseCur = GBP - def convertedMinFloorValueGbp = getPriceAfterCurrencyConversion(floorValue, + def convertedMinFloorValueGbp = CurrencyUtil.getPriceAfterCurrencyConversion(floorValue, floorCur, bidResponseCur, currencyRatesResponse) def winBidPrice = convertedMinFloorValueGbp + 0.1 def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -345,7 +358,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } and: "PBS should suppress bids lower than floorRuleValue" - def convertedFloorValueEur = getPriceAfterCurrencyConversion(winBidPrice, + def convertedFloorValueEur = CurrencyUtil.getPriceAfterCurrencyConversion(winBidPrice, bidResponseCur, requestCur, currencyRatesResponse) assert response.seatbid?.first()?.bid?.collect { it.price } == [convertedFloorValueEur] assert response.cur == bidRequest.cur[0] @@ -367,6 +380,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = BOGUS def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -413,6 +430,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -470,6 +491,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -496,6 +521,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy index a885da9e86b..b06c2530242 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy @@ -18,6 +18,7 @@ import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.MediaType import org.prebid.server.functional.util.PBSUtils +import java.math.RoundingMode import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 @@ -1145,4 +1146,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { private static int getRuleSize(BidRequest bidRequest) { bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].values.size() } + + private static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { + floorValue.divide(bidAdjustment, FLOOR_VALUE_PRECISION, RoundingMode.HALF_UP) + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy b/src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy new file mode 100644 index 00000000000..172478ad5ba --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy @@ -0,0 +1,74 @@ +package org.prebid.server.functional.util + +import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse + +import java.math.RoundingMode + +import static org.prebid.server.functional.model.Currency.CAD +import static org.prebid.server.functional.model.Currency.CHF +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.Currency.USD + +class CurrencyUtil { + + public static final Currency DEFAULT_CURRENCY = USD + public static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9249838127832763, + (GBP): 0.793776804452961, + (EUR): 0.9249838127832763, + (CHF): 0.9033391915641477, + (JPY): 151.1886041994265, + (CAD): 1.357136250115623], + (GBP): [(USD): 1.2597999770088517, + (EUR): 1.1495574203931487], + (EUR): [(USD): 1.3429368029739777]] + public static final int PRICE_PRECISION = 3 + public static final int CURRENCY_CONVERSION_PRECISION = 3 + + static BigDecimal getPriceAfterCurrencyConversion(BigDecimal value, + Currency from, + Currency to, + CurrencyRatesResponse currencyRatesResponse) { + (value * currencyRatesResponse.rates[from.value][to.value]) + .setScale(CURRENCY_CONVERSION_PRECISION, RoundingMode.HALF_EVEN) + } + + static BigDecimal convertCurrency(BigDecimal price, + Currency fromCurrency, + Currency toCurrency, + Map> rates = DEFAULT_CURRENCY_RATES) { + return (price * getConversionRate(fromCurrency, toCurrency, rates)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) + } + + private static BigDecimal getConversionRate(Currency fromCurrency, + Currency toCurrency, + Map> rates = DEFAULT_CURRENCY_RATES) { + def conversionRate + if (fromCurrency == toCurrency) { + conversionRate = 1 + } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { + conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] + } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { + conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + } else { + conversionRate = getCrossConversionRate(fromCurrency, toCurrency, rates) + } + conversionRate + } + + private static BigDecimal getCrossConversionRate(Currency fromCurrency, + Currency toCurrency, + Map> rates) { + + for (Map rate : rates.values()) { + def fromRate = rate?[fromCurrency] + def toRate = rate?[toCurrency] + if (fromRate && toRate) { + return toRate / fromRate + } + } + null + } +} diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java index 9bfbc9cb143..4d0dc685827 100644 --- a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -65,7 +65,7 @@ public void setUp() { given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); - given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any())) .willAnswer(inv -> inv.getArgument(0)); target = new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); @@ -88,7 +88,7 @@ public void shouldReturnBidsAdjustedByBidAdjustmentsProcessor() { final BidderBid adjustedBid = givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.TEN).build(), "USD"); - given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any())) .willReturn(AuctionParticipation.builder() .bidder("bidder1") .bidderResponse(BidderResponse.of( diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 584825fa273..a913772645e 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -1241,6 +1241,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .build())))) + .originalPriceFloors(Collections.emptyMap()) .build()), any(), any(), @@ -1263,6 +1264,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .build())))) + .originalPriceFloors(Collections.emptyMap()) .build()), any(), any(), diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 4da083fac4e..19e414e80c9 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -37,9 +37,8 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; -import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.bidadjustments.model.BidAdjustmentType; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.geolocation.model.GeoInfo; @@ -53,7 +52,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; @@ -64,7 +63,6 @@ import java.util.Map; import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -111,7 +109,7 @@ public class AuctionRequestFactoryTest extends VertxTest { @Mock(strictness = LENIENT) private GeoLocationServiceWrapper geoLocationServiceWrapper; @Mock(strictness = LENIENT) - private BidAdjustmentsRetriever bidAdjustmentsRetriever; + private BidAdjustmentsEnricher bidAdjustmentsEnricher; private AuctionRequestFactory target; @@ -201,7 +199,7 @@ public void setUp() { .will(invocationOnMock -> invocationOnMock.getArgument(0)); given(geoLocationServiceWrapper.lookup(any())) .willReturn(Future.succeededFuture(GeoInfo.builder().vendor("vendor").build())); - given(bidAdjustmentsRetriever.retrieve(any())).willReturn(BidAdjustments.of(emptyMap())); + given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(defaultBidRequest); target = new AuctionRequestFactory( Integer.MAX_VALUE, @@ -218,7 +216,7 @@ public void setUp() { debugResolver, jacksonMapper, geoLocationServiceWrapper, - bidAdjustmentsRetriever); + bidAdjustmentsEnricher); } @Test @@ -254,7 +252,7 @@ public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() { debugResolver, jacksonMapper, geoLocationServiceWrapper, - bidAdjustmentsRetriever); + bidAdjustmentsEnricher); given(requestBody.asString()).willReturn("body"); @@ -697,6 +695,7 @@ public void shouldReturnModifiedBidRequestInAuctionContextWhenRequestWasPopulate final BidRequest updatedBidRequest = defaultBidRequest.toBuilder().id("updated").build(); given(paramsResolver.resolve(any(), any(), any(), anyBoolean())).willReturn(updatedBidRequest); + given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(updatedBidRequest); // when final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); @@ -733,22 +732,30 @@ public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementRetur @Test public void shouldReturnPopulatedBidAdjustments() { // given - givenValidBidRequest(); - - final BidAdjustments bidAdjustments = BidAdjustments.of(Map.of( - "rule1", List.of( - ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()), - "rule2", List.of( - ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(), - ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build()))); + final ObjectNode bidAdjustments = mapper.valueToTree(Map.of( + "mediaType1", Map.of("bidder1", Map.of("dealId1", List.of( + BidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()))), + "mediaType2", Map.of("bidder2", Map.of("dealId2", List.of( + BidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(), + BidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build()))))); + + final BidRequest givenBidRequest = defaultBidRequest.toBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(bidAdjustments).build())) + .build(); - given(bidAdjustmentsRetriever.retrieve(any())).willReturn(bidAdjustments); + givenValidBidRequest(givenBidRequest); + given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(givenBidRequest); // when final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); // then - assertThat(result.getBidAdjustments()).isEqualTo(bidAdjustments); + assertThat(result) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(bidAdjustments); } @Test diff --git a/src/test/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolverTest.java similarity index 98% rename from src/test/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolverTest.java rename to src/test/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolverTest.java index f9267899fd5..a1a7cd0b072 100644 --- a/src/test/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolverTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.junit.jupiter.api.Test; import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; @@ -122,7 +122,7 @@ public void resolveShouldReturnAdjustmentByMediaTypeIfPresentIgnoringCase() { adjustmentFactors.addFactor("BIDder", BigDecimal.valueOf(5.456)); // when - final BigDecimal result = target.resolve(ImpMediaType.video, adjustmentFactors, "bidDER", "Seat"); + final BigDecimal result = target.resolve(ImpMediaType.video_instream, adjustmentFactors, "bidDER", "Seat"); // then assertThat(result).isEqualTo(BigDecimal.valueOf(1.234)); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java index 0c98ff6af3b..a9634e568fd 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java @@ -1,8 +1,8 @@ package org.prebid.server.bidadjustments; import org.junit.jupiter.api.Test; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.validation.ValidationException; import java.math.BigDecimal; @@ -28,28 +28,26 @@ public void validateShouldDoNothingWhenBidAdjustmentsIsNull() throws ValidationE @Test public void validateShouldDoNothingWhenMediatypesIsEmpty() throws ValidationException { // when & then - BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder().build()); + BidAdjustmentRulesValidator.validate(BidAdjustments.of(Collections.emptyMap())); } @Test public void validateShouldSkipMediatypeValidationWhenMediatypesIsNotSupported() throws ValidationException { // given - final ExtRequestBidAdjustmentsRule invalidRule = ExtRequestBidAdjustmentsRule.builder() + final BidAdjustmentsRule invalidRule = BidAdjustmentsRule.builder() .value(new BigDecimal("-999")) .build(); // when & then - BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() - .mediatype(Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule))))) - .build()); + BidAdjustmentRulesValidator.validate(BidAdjustments.of( + Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule)))))); } @Test public void validateShouldFailWhenBiddersAreAbsent() { // given - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Collections.emptyMap())) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Collections.emptyMap())); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -60,9 +58,8 @@ public void validateShouldFailWhenBiddersAreAbsent() { @Test public void validateShouldFailWhenDealsAreAbsent() { // given - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", Collections.emptyMap()))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", Collections.emptyMap()))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -73,12 +70,11 @@ public void validateShouldFailWhenDealsAreAbsent() { @Test public void validateShouldFailWhenRulesIsEmpty() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", null); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -89,24 +85,23 @@ public void validateShouldFailWhenRulesIsEmpty() { @Test public void validateShouldDoNothingWhenRulesAreEmpty() throws ValidationException { // when & then - BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() - .mediatype(Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of())))) - .build()); + BidAdjustmentRulesValidator.validate(BidAdjustments.of( + Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of()))))); + } @Test public void validateShouldFailWhenRuleHasUnknownType() { // given - final Map> rules = new HashMap<>(); - rules.put("*", List.of(ExtRequestBidAdjustmentsRule.builder() + final Map> rules = new HashMap<>(); + rules.put("*", List.of(BidAdjustmentsRule.builder() .adjType(UNKNOWN) .value(BigDecimal.ONE) .currency("USD") .build())); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -118,12 +113,11 @@ public void validateShouldFailWhenRuleHasUnknownType() { @Test public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenCpm("1", "USD"), givenCpm("1", null))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -134,12 +128,11 @@ public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() { @Test public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("-1", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -150,12 +143,11 @@ public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() { @Test public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("2147483647", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -167,12 +159,11 @@ public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() { @Test public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenStatic("1", "USD"), givenStatic("1", null))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -184,12 +175,11 @@ public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() { @Test public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("-1", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -201,12 +191,11 @@ public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() { @Test public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("2147483647", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -218,12 +207,11 @@ public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() { @Test public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("-1"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -235,12 +223,11 @@ public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() { @Test public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("100"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -252,17 +239,16 @@ public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() { @Test public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationException { // given - final List givenRules = List.of( + final List givenRules = List.of( givenMultiplier("1"), givenCpm("2", "USD"), givenStatic("3", "EUR")); - final Map>> givenRulesMap = Map.of( + final Map>> givenRulesMap = Map.of( "bidderName", Map.of("dealId", givenRules)); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of( + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( "audio", givenRulesMap, "native", givenRulesMap, "video-instream", givenRulesMap, @@ -274,31 +260,30 @@ public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationExcep "*", Map.of("*", givenRules), "bidderName", Map.of( "*", givenRules, - "dealId", givenRules)))) - .build(); + "dealId", givenRules)))); //when & then BidAdjustmentRulesValidator.validate(givenBidAdjustments); } - private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenStatic(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(STATIC) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenCpm(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(CPM) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenMultiplier(String value) { + return BidAdjustmentsRule.builder() .adjType(MULTIPLIER) .value(new BigDecimal(value)) .build(); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricherTest.java similarity index 65% rename from src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java rename to src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricherTest.java index df6caa05abd..e5997dbfd6c 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricherTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidadjustments; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import org.junit.jupiter.api.BeforeEach; @@ -8,49 +9,47 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.debug.DebugContext; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; -import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; -import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; -public class BidAdjustmentsRetrieverTest extends VertxTest { +public class BidAdjustmentsEnricherTest extends VertxTest { - private BidAdjustmentsRetriever target; + private BidAdjustmentsEnricher target; @BeforeEach public void before() { - target = new BidAdjustmentsRetriever(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d); + target = new BidAdjustmentsEnricher(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d); } @Test - public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() { + public void enrichBidRequestShouldReturnEmptyAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() { // given final List debugMessages = new ArrayList<>(); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( null, null, debugMessages, true)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages).isEmpty(); } @Test - public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountAdjustmentsAreAbsent() + public void enrichBidRequestShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountRequestAreAbsent() throws JsonProcessingException { // given @@ -63,7 +62,7 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccoun "invalid": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -76,18 +75,22 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccoun final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, null, debugMessages, true)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages) .containsOnly("bid adjustment from request was invalid: the found rule " + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.invalid.invalid is invalid"); } @Test - public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAbsent() + public void enrichBidRequestShouldReturnRequestBidAdjustmentsWhenAccountRequestAreAbsent() throws JsonProcessingException { // given @@ -100,7 +103,7 @@ public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAb "*": [ { "adjtype": "cpm", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -113,24 +116,22 @@ public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAb final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, null, debugMessages, true)); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( - "banner|*|*", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(CPM) - .currency("USD") - .value(new BigDecimal("0.1")) - .build()))); - - assertThat(actual).isEqualTo(expected); + final JsonNode expected = givenRule("banner", "*", "*", "cpm", "0.1", "USD"); + + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(expected); assertThat(debugMessages).isEmpty(); } @Test - public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAbsent() + public void enrichBidRequestShouldReturnAccountBidAdjustmentsWhenRequestRequestAreAbsent() throws JsonProcessingException { // given @@ -143,7 +144,7 @@ public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAb "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -161,7 +162,7 @@ public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAb "*": [ { "adjtype": "static", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -175,26 +176,24 @@ public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAb final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( - "audio|bidder|*", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(STATIC) - .currency("USD") - .value(new BigDecimal("0.1")) - .build()))); - - assertThat(actual).isEqualTo(expected); + final JsonNode expected = givenRule("audio", "bidder", "*", "static", "0.1", "USD"); + + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(expected); assertThat(debugMessages) .containsOnly("bid adjustment from request was invalid: the found rule " + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.*.* is invalid"); } @Test - public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustmentsAreInvalid() + public void enrichBidRequestShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestRequestAreInvalid() throws JsonProcessingException { // given @@ -207,7 +206,7 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustme "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -225,7 +224,7 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustme "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -239,20 +238,24 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustme final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages).containsExactlyInAnyOrder( - "bid adjustment from request was invalid: the found rule " - + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid", - "bid adjustment from account was invalid: the found rule " - + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid"); + "bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid", + "bid adjustment from account was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid"); } @Test - public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException { + public void enrichBidRequestShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException { // given final List debugMessages = new ArrayList<>(); final String requestAdjustments = """ @@ -263,7 +266,7 @@ public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws Js "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -281,7 +284,7 @@ public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws Js "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -295,16 +298,20 @@ public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws Js final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, false)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages).isEmpty(); } @Test - public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws JsonProcessingException { + public void enrichBidRequestShouldReturnMergedAccountIntoRequestRequest() throws JsonProcessingException { // given final List debugMessages = new ArrayList<>(); final String requestAdjustments = """ @@ -315,7 +322,7 @@ public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws Jso "*": [ { "adjtype": "cpm", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -333,14 +340,14 @@ public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws Jso "dealId": [ { "adjtype": "cpm", - "value": 0.3, + "value": "0.3", "currency": "USD" } ], "*": [ { "adjtype": "static", - "value": 0.2, + "value": "0.2", "currency": "USD" } ] @@ -354,25 +361,22 @@ public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws Jso final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( - "banner|*|dealId", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(CPM) - .currency("USD") - .value(new BigDecimal("0.3")) - .build()), - "banner|*|*", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(CPM) - .currency("USD") - .value(new BigDecimal("0.1")) - .build()))); - - assertThat(actual).isEqualTo(expected); + final JsonNode expected = mapper.valueToTree( + Map.of("mediatype", Map.of("banner", Map.of("*", Map.of( + "*", mapper.createArrayNode().add(mapper.createObjectNode() + .put("adjtype", "cpm").put("value", "0.1").put("currency", "USD")), + "dealId", mapper.createArrayNode().add(mapper.createObjectNode() + .put("adjtype", "cpm").put("value", "0.3").put("currency", "USD"))))))); + + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(expected); assertThat(debugMessages).isEmpty(); } @@ -393,4 +397,19 @@ private static AuctionContext givenAuctionContext(ObjectNode requestBidAdjustmen .build(); } + private static JsonNode givenRule(String mediaType, + String bidder, + String dealId, + String adjtype, + String value, + String currency) { + + return mapper.valueToTree( + Map.of("mediatype", Map.of(mediaType, Map.of(bidder, Map.of(dealId, mapper.createArrayNode() + .add(mapper.createObjectNode() + .put("adjtype", adjtype) + .put("value", value) + .put("currency", currency))))))); + } + } diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java index 2875d044cad..375ece90181 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java @@ -13,11 +13,10 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.prebid.server.VertxTest; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; @@ -26,7 +25,7 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; @@ -35,6 +34,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import java.math.BigDecimal; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; @@ -91,8 +91,11 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { // given final BidderResponse bidderResponse = givenBidderResponse( Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build()); + final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); @@ -104,8 +107,7 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -115,8 +117,8 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2.0))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -126,8 +128,11 @@ public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { // given final BidderResponse bidderResponse = givenBidderResponse( Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); @@ -137,8 +142,7 @@ public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -152,8 +156,11 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { // given final BidderResponse bidderResponse = givenBidderResponse( Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); @@ -161,8 +168,7 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then final BidderError expectedError = BidderError.generic( @@ -178,7 +184,9 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving( final BidderResponse bidderResponse = givenBidderResponse( Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); @@ -188,8 +196,7 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving( .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then final BidderError expectedError = BidderError.generic( @@ -218,6 +225,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); @@ -232,8 +240,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then final BidderSeatBid seatBid = result.getBidderResponse().getSeatBid(); @@ -245,8 +252,8 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(20.0))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -277,8 +284,7 @@ public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForS final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, null); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); @@ -312,10 +318,12 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported .build(), 1); + final ObjectNode bidAdjustments = mapper.valueToTree(givenBidAdjustments()); final BidRequest bidRequest = BidRequest.builder() .cur(singletonList("CUR")) - .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), - identity()))).build(); + .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), identity()))) + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(bidAdjustments).build())) + .build(); final BigDecimal updatedPrice = BigDecimal.valueOf(20); given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); @@ -326,7 +334,7 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported // when final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + auctionParticipation, bidRequest); // then verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("CUR")); @@ -363,7 +371,11 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { final BidRequest bidRequest = givenBidRequest( singletonList(givenImp(Map.of("bidder1", 1), identity())), - builder -> builder.cur(singletonList("USD"))); + builder -> builder + .cur(singletonList("USD")) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) + .build()))); final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); @@ -372,8 +384,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); @@ -411,14 +422,14 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -429,8 +440,8 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(4.936))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -454,7 +465,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, singletonMap("bidder", BigDecimal.valueOf(3.456))))) .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "adapter", "seat")) + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_instream, givenAdjustments, "adapter", "seat")) .willReturn(BigDecimal.valueOf(3.456)); final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> @@ -462,14 +473,14 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -480,8 +491,8 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_instream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -505,7 +516,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, singletonMap("bidder", BigDecimal.valueOf(3.456))))) .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "adapter", "seat")) + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_instream, givenAdjustments, "adapter", "seat")) .willReturn(BigDecimal.valueOf(3.456)); final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> @@ -513,14 +524,14 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -531,8 +542,8 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_instream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -567,13 +578,13 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideo builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -584,8 +595,8 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideo verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_outstream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -615,14 +626,14 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidIm builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -634,8 +645,8 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidIm verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_outstream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -666,14 +677,14 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpId builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -684,8 +695,8 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpId verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_outstream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -714,14 +725,14 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -729,8 +740,7 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() .extracting(Bid::getPrice) .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - verify(bidAdjustmentsResolver, times(3)) - .resolve(any(), any(), any(), any(), any(), any()); + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any()); } @Test @@ -763,14 +773,14 @@ public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -781,8 +791,8 @@ public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -812,13 +822,13 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent .auctiontimestamp(1000L) .currency(ExtRequestCurrency.of(null, false)) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -829,8 +839,8 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.ONE)), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -866,8 +876,8 @@ private static Map doubleMap(K key1, V value1, K key2, V value2) { return map; } - private static BidAdjustments givenBidAdjustments() { - return BidAdjustments.of(ExtRequestBidAdjustments.builder().build()); + private static BidAdjustmentsRules givenBidAdjustments() { + return BidAdjustmentsRules.of(BidAdjustments.of(Collections.emptyMap())); } private BidderResponse givenBidderResponse(Bid bid) { diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java index 97ca68e939e..3bc13250fe4 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java @@ -7,15 +7,13 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.bidder.model.Price; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.math.BigDecimal; +import java.util.Collections; import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -26,6 +24,8 @@ import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.banner; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.video_outstream; @ExtendWith(MockitoExtension.class) public class BidAdjustmentsResolverTest extends VertxTest { @@ -33,11 +33,14 @@ public class BidAdjustmentsResolverTest extends VertxTest { @Mock(strictness = LENIENT) private CurrencyConversionService currencyService; + @Mock(strictness = LENIENT) + private BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + private BidAdjustmentsResolver target; @BeforeEach public void before() { - target = new BidAdjustmentsResolver(currencyService); + target = new BidAdjustmentsResolver(currencyService, bidAdjustmentsRulesResolver); given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> { final BigDecimal initialPrice = (BigDecimal) invocation.getArguments()[0]; @@ -46,18 +49,18 @@ public void before() { } @Test - public void resolveShouldPickAndApplyRulesBySpecificMediaType() { + public void resolveShouldApplyStaticRule() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "banner|*|*", List.of(givenStatic("15", "EUR")), - "*|*|*", List.of(givenStatic("25", "UAH")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) + .willReturn(List.of(givenStatic("15", "EUR"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), - BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + givenBidRequest, + banner, + "seat", "bidderName", "dealId"); @@ -67,20 +70,18 @@ public void resolveShouldPickAndApplyRulesBySpecificMediaType() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardMediaType() { + public void resolveShouldApplyCpmRule() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "banner|*|*", List.of(givenCpm("15", "EUR")), - "*|*|*", List.of(givenCpm("25", "UAH")))); - final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, video_outstream, "seat", "bidderName", "dealId")) + .willReturn(List.of(givenCpm("25", "UAH"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.video_outstream, + video_outstream, + "seat", "bidderName", "dealId"); @@ -90,18 +91,18 @@ public void resolveShouldPickAndApplyRulesByWildcardMediaType() { } @Test - public void resolveShouldPickAndApplyRulesBySpecificBidder() { + public void resolveShouldApplyMultiplierRule() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|bidderName|*", List.of(givenMultiplier("15")), - "*|*|*", List.of(givenMultiplier("25")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) + .willReturn(List.of(givenMultiplier("15"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + banner, + "seat", "bidderName", "dealId"); @@ -111,19 +112,19 @@ public void resolveShouldPickAndApplyRulesBySpecificBidder() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardBidder() { + public void resolveShouldApplyStaticAndCpmRules() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|bidderName|*", List.of(givenStatic("15", "EUR"), givenMultiplier("15")), - "*|*|*", List.of(givenStatic("25", "UAH"), givenMultiplier("25")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) + .willReturn(List.of(givenStatic("25", "UAH"), givenMultiplier("25"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, - "anotherBidderName", + banner, + "seat", + "bidderName", "dealId"); // then @@ -132,19 +133,18 @@ public void resolveShouldPickAndApplyRulesByWildcardBidder() { } @Test - public void resolveShouldPickAndApplyRulesBySpecificDealId() { + public void resolveShouldApplyCpmAndStaticRules() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR")), - "*|*|*", List.of(givenCpm("25", "JPY"), givenStatic("25", "UAH")))); final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) + .willReturn(List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.banner, + banner, + "seat", "bidderName", "dealId"); @@ -154,21 +154,20 @@ public void resolveShouldPickAndApplyRulesBySpecificDealId() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardDealId() { + public void resolveShouldApplyMultiplierAdnCpmRules() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenMultiplier("15"), givenCpm("15", "EUR")), - "*|*|*", List.of(givenMultiplier("25"), givenCpm("25", "UAH")))); final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) + .willReturn(List.of(givenMultiplier("25"), givenCpm("25", "UAH"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.banner, + banner, + "seat", "bidderName", - "anotherDealId"); + "dealId"); // then assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-225"))); @@ -176,19 +175,18 @@ public void resolveShouldPickAndApplyRulesByWildcardDealId() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { + public void resolveShouldApplyTwoCpmRules() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenCpm("15", "EUR"), givenCpm("15", "JPY")), - "*|*|*", List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY")))); final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", null)) + .willReturn(List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.banner, + banner, + "seat", "bidderName", null); @@ -199,17 +197,18 @@ public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { } @Test - public void resolveShouldReturnEmptyListWhenNoMatchFound() { + public void resolveShouldNotApplyAnyRulesWhenNoMatchFound() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenStatic("15", "EUR")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", null)) + .willReturn(Collections.emptyList()); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + banner, + "seat", "bidderName", null); @@ -218,24 +217,24 @@ public void resolveShouldReturnEmptyListWhenNoMatchFound() { verifyNoInteractions(currencyService); } - private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenStatic(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(STATIC) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenCpm(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(CPM) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenMultiplier(String value) { + return BidAdjustmentsRule.builder() .adjType(MULTIPLIER) .value(new BigDecimal(value)) .build(); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java new file mode 100644 index 00000000000..2c85847fd63 --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java @@ -0,0 +1,694 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; + +public class BidAdjustmentsRulesResolverTest extends VertxTest { + + private final BidAdjustmentsRulesResolver target = new BidAdjustmentsRulesResolver(jacksonMapper); + + @Test + public void resolveShouldPickAndApplyRulesBySpecificMediaType() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ] + } + }, + "*": { + "*": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("15", "EUR")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardMediaType() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": "15", + "currency": "EUR" + } + ] + } + }, + "*": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.video_outstream, + "seat", + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedCpm("25", "UAH")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidder() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardBidder() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "anotherBidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "UAH"), expectedMultiplier("25")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificDealId() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "cpm", + "value": "15", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ], + "*": [ + { + "adjtype": "cpm", + "value": "25", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedCpm("15", "JPY"), expectedStatic("15", "EUR")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealId() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "multiplier", + "value": "15" + }, + { + "adjtype": "cpm", + "value": "15", + "currency": "EUR" + } + ], + "*": [ + { + "adjtype": "multiplier", + "value": "25" + }, + { + "adjtype": "cpm", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + "anotherDealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("25"), expectedCpm("25", "UAH")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "cpm", + "value": "15", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ], + "*": [ + { + "adjtype": "cpm", + "value": "25", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + null); + + // then + assertThat(actual).containsExactly(expectedCpm("25", "JPY"), expectedStatic("25", "UAH")); + } + + @Test + public void resolveShouldReturnEmptyListWhenNoMatchFound() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + null); + + // then + assertThat(actual).isEmpty(); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificSeat() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "seat": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + null); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardSeat() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "seat": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "anotherBidderName", + "anotherSeat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "UAH"), expectedMultiplier("25")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidderOverAbsentSeat() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "seat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidderCaseInsensitive() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "BIDDERname": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "seat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificSeatOverSpecificBidder() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "seat": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "UAH"), expectedMultiplier("25")); + } + + @Test + public void resolveShouldPickAndApplyMoreSpecificRuleOverLessSpecific() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "banner": { + "bidderName": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ], + "dealId": [ + { + "adjtype": "static", + "value": "25", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + }, + "seat": { + "*": [ + { + "adjtype": "static", + "value": "35", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "35" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "seat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "EUR"), expectedMultiplier("25")); + } + + private static BidRequest givenBidRequest(String adjustmentsString) throws JsonProcessingException { + final ObjectNode adjustmetsNode = (ObjectNode) mapper.readTree(adjustmentsString); + return BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(adjustmetsNode).build())) + .build(); + } + + private static BidAdjustmentsRule expectedStatic(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule expectedCpm(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule expectedMultiplier(String value) { + return BidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolverTest.java similarity index 99% rename from src/test/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolverTest.java rename to src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolverTest.java index 0c0ab6b155f..9f78d514a3a 100644 --- a/src/test/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolverTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java new file mode 100644 index 00000000000..1cfb5dbe848 --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java @@ -0,0 +1,235 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.banner; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.video_outstream; + +@ExtendWith(MockitoExtension.class) +public class FloorAdjustmentsResolverTest extends VertxTest { + + private static final String BIDDER_NAME = "testBidder"; + private static final String USD = "USD"; + private static final String EUR = "EUR"; + private static final String UAH = "UAH"; + + @Mock + private CurrencyConversionService currencyService; + + @Mock(strictness = LENIENT) + private BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + + private FloorAdjustmentsResolver target; + + @BeforeEach + public void before() { + target = new FloorAdjustmentsResolver(bidAdjustmentsRulesResolver, currencyService); + } + + @Test + public void resolveShouldReturnInitialPriceWhenNoRulesFoundForAnyMediaType() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.TEN); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner, video_outstream); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)).willReturn(emptyList()); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME)).willReturn(emptyList()); + given(currencyService.convertCurrency(BigDecimal.TEN, bidRequest, USD, USD)).willReturn(BigDecimal.TEN); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(initialPrice); + verify(currencyService, times(2)).convertCurrency(any(), any(), any(), any()); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldApplyMultiplierRuleInReverse() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(20)); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule multiplierRule = givenMultiplier("2"); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)) + .willReturn(singletonList(multiplierRule)); + given(currencyService.convertCurrency(eq(new BigDecimal("10.0000")), eq(bidRequest), eq(USD), eq(USD))) + .willReturn(BigDecimal.valueOf(20)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("10.0000"))); + + verify(currencyService).convertCurrency(any(), any(), any(), any()); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldApplyCpmRuleInReverse() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(50)); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule cpmRule = givenCpm("5", EUR); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)).willReturn(singletonList(cpmRule)); + given(currencyService.convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD)) + .willReturn(BigDecimal.valueOf(0.5)); + given(currencyService.convertCurrency(new BigDecimal("50.5"), bidRequest, USD, USD)) + .willReturn(BigDecimal.valueOf(50)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + final Price expectedPrice = Price.of(USD, new BigDecimal("50.5")); + assertThat(actual).isEqualTo(expectedPrice); + + verify(currencyService, times(2)).convertCurrency(any(), any(), any(), any()); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldThrowExceptionWhenStaticRuleIsEncountered() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.TEN); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule staticRule = givenStatic("5", USD); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)) + .willReturn(singletonList(staticRule)); + + // when and then + assertThatThrownBy(() -> target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME)) + .isInstanceOf(PreBidException.class) + .hasMessage("STATIC type can't be applied to a floor price"); + + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldApplyMultipleRulesInReverseOrder() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(100)); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule rule1 = givenMultiplier("2"); + final BidAdjustmentsRule rule2 = givenCpm("5", EUR); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)).willReturn(List.of(rule1, rule2)); + given(currencyService.convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD)) + .willReturn(BigDecimal.valueOf(0.5)); + given(currencyService.convertCurrency(new BigDecimal("50.2500"), bidRequest, USD, USD)) + .willReturn(BigDecimal.valueOf(100)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("50.2500"))); + + verify(currencyService, times(2)).convertCurrency(any(), any(), any(), any()); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldChooseMinimalFloorAcrossMediaTypesAfterConversion() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(100)); + final BidRequest bidRequest = givenBidRequest(EUR); + final Set mediaTypes = Set.of(banner, video_outstream); + + final BidAdjustmentsRule bannerRule = givenMultiplier("4"); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)) + .willReturn(singletonList(bannerRule)); + + final BidAdjustmentsRule videoRule = givenCpm("500", UAH); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME)) + .willReturn(singletonList(videoRule)); + + given(currencyService.convertCurrency(new BigDecimal("25.0000"), bidRequest, USD, EUR)) + .willReturn(new BigDecimal("250.0000")); + given(currencyService.convertCurrency(new BigDecimal("500"), bidRequest, UAH, USD)) + .willReturn(new BigDecimal("50")); + given(currencyService.convertCurrency(new BigDecimal("150"), bidRequest, USD, EUR)) + .willReturn(new BigDecimal("1500")); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("25.0000"))); + + verify(currencyService, times(3)).convertCurrency(any(), any(), any(), any()); + verifyNoMoreInteractions(currencyService); + } + + private static BidRequest givenBidRequest(String currency) { + return BidRequest.builder() + .cur(singletonList(currency)) + .build(); + } + + private static BidAdjustmentsRule givenStatic(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule givenCpm(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule givenMultiplier(String value) { + return BidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java similarity index 65% rename from src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java rename to src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java index 6bc26d7ef1a..35afb5ca46d 100644 --- a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java @@ -1,8 +1,7 @@ package org.prebid.server.bidadjustments.model; +import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.junit.jupiter.api.Test; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import java.math.BigDecimal; import java.util.List; @@ -11,18 +10,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; -public class BidAdjustmentsTest { +public class BidAdjustmentsRulesTest { @Test public void shouldBuildRulesSet() { // given - final List givenRules = List.of(givenRule("1"), givenRule("2")); - final Map>> givenRulesMap = Map.of( + final List givenRules = List.of(givenRule("1"), givenRule("2")); + final Map>> givenRulesMap = Map.of( "bidderName", Map.of("dealId", givenRules)); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of( + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of( "audio", givenRulesMap, "native", givenRulesMap, "video-instream", givenRulesMap, @@ -34,14 +33,13 @@ public void shouldBuildRulesSet() { "*", Map.of("*", givenRules), "bidderName", Map.of( "*", givenRules, - "dealId", givenRules)))) - .build(); + "dealId", givenRules)))); // when - final BidAdjustments actual = BidAdjustments.of(givenBidAdjustments); + final BidAdjustmentsRules actual = BidAdjustmentsRules.of(givenBidAdjustments); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( + final BidAdjustmentsRules expected = BidAdjustmentsRules.of(new CaseInsensitiveMap<>(Map.of( "audio|bidderName|dealId", givenRules, "native|bidderName|dealId", givenRules, "video-instream|bidderName|dealId", givenRules, @@ -49,17 +47,17 @@ public void shouldBuildRulesSet() { "banner|bidderName|dealId", givenRules, "*|*|*", givenRules, "*|bidderName|*", givenRules, - "*|bidderName|dealId", givenRules)); + "*|bidderName|dealId", givenRules))); assertThat(actual).isEqualTo(expected); - } - private static ExtRequestBidAdjustmentsRule givenRule(String value) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenRule(String value) { + return BidAdjustmentsRule.builder() .adjType(CPM) .currency("USD") .value(new BigDecimal(value)) .build(); } + } diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java index 1aecc95bdf8..20d7308a271 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java @@ -9,8 +9,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.bidder.model.Price; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -25,15 +27,19 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.Map; +import java.util.Set; import java.util.function.UnaryOperator; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.video_instream; @ExtendWith(MockitoExtension.class) public class BasicPriceFloorAdjusterTest extends VertxTest { @@ -43,35 +49,48 @@ public class BasicPriceFloorAdjusterTest extends VertxTest { @Mock(strictness = LENIENT) private FloorAdjustmentFactorResolver floorAdjustmentFactorResolver; + @Mock(strictness = LENIENT) + private FloorAdjustmentsResolver floorAdjustmentsResolver; + private BasicPriceFloorAdjuster target; @BeforeEach public void setUp() { given(floorAdjustmentFactorResolver.resolve(anySet(), any(), any())).willReturn(BigDecimal.ONE); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); - target = new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver); + target = new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver, floorAdjustmentsResolver); } @Test - public void adjustForImpShouldCallAdjustmentFactorResolverAndApplyFactor() { + public void adjustForImpShouldApplyAllAdjustments() { // given given(floorAdjustmentFactorResolver.resolve(anySet(), any(), any())).willReturn(new BigDecimal("0.1")); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willReturn(Price.of("UAH", new BigDecimal("117.00"))); + final BidRequest givenBidRequest = givenBidRequest(identity()); // when final Price adjustedBidPrice = target.adjustForImp( givenImp(identity()), RUBICON, - givenBidRequest(identity()), + givenBidRequest, null, new ArrayList<>()); // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", new BigDecimal(100))); - verify(floorAdjustmentFactorResolver).resolve(anySet(), any(), any()); + assertThat(adjustedBidPrice).isEqualTo(Price.of("UAH", new BigDecimal("117.00"))); + verify(floorAdjustmentFactorResolver).resolve(eq(Set.of(video_instream)), any(), eq(RUBICON)); + verify(floorAdjustmentsResolver).resolve( + eq(Price.of("USD", new BigDecimal(100))), + eq(givenBidRequest), + eq(Set.of(video_instream)), + eq(RUBICON)); } @Test - public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByAccount() { + public void adjustForImpShouldNotApplyAdjustmentsWhenAdjustmentDisabledByAccount() { // given final Account account = Account.builder() .auction(AccountAuctionConfig.builder() @@ -91,10 +110,11 @@ public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByAccount() { // then assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); + verifyNoInteractions(floorAdjustmentsResolver, floorAdjustmentFactorResolver); } @Test - public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByRequest() { + public void adjustForImpShouldNotApplyAdjustmentsWhenAdjustmentDisabledByRequest() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() @@ -120,13 +140,16 @@ public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByRequest() { // then assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); + verifyNoInteractions(floorAdjustmentsResolver, floorAdjustmentFactorResolver); } @Test - public void adjustForImpShouldApplyNoAdjustmentsIfBidAdjustmentsFactorIsNotPresent() { + public void adjustForImpShouldApplyNoFactorAdjustmentsWhenBidAdjustmentsFactorIsNotPresent() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustmentfactors(null).build()))); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willReturn(Price.of("UAH", new BigDecimal("117.00"))); final Imp imp = givenImp(identity()); // when @@ -138,7 +161,13 @@ public void adjustForImpShouldApplyNoAdjustmentsIfBidAdjustmentsFactorIsNotPrese new ArrayList<>()); // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", imp.getBidfloor())); + assertThat(adjustedBidPrice).isEqualTo(Price.of("UAH", new BigDecimal("117.00"))); + verifyNoInteractions(floorAdjustmentFactorResolver); + verify(floorAdjustmentsResolver).resolve( + eq(Price.of(imp.getBidfloorcur(), imp.getBidfloor())), + eq(bidRequest), + eq(Set.of(video_instream)), + eq(RUBICON)); } @Test @@ -156,6 +185,7 @@ public void adjustForImpShouldReturnNullIfImpBidFloorIsNotPresent() { // then assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", null)); + verifyNoInteractions(floorAdjustmentFactorResolver, floorAdjustmentsResolver); } @Test @@ -247,188 +277,23 @@ public void adjustForImpShouldSkipMediaTypeIfNoMediaTypesOfImpFound() { } @Test - public void revertAdjustmentForImpShouldCallAdjustmentFactorResolverAndApplyFactor() { + public void adjustForImpShouldSkipBidAdjustmentsWhenResolverThrowsException() { // given given(floorAdjustmentFactorResolver.resolve(anySet(), any(), any())).willReturn(new BigDecimal("0.1")); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willThrow(new PreBidException("Exception")); + final BidRequest givenBidRequest = givenBidRequest(identity()); // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - givenBidRequest(identity()), - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.ONE)); - verify(floorAdjustmentFactorResolver).resolve(anySet(), any(), any()); - } - - @Test - public void revertAdjustmentForImpShouldNotApplyFactorIfAdjustmentDisabledByAccount() { - // given - final Account account = Account.builder() - .auction(AccountAuctionConfig.builder() - .priceFloors(AccountPriceFloorsConfig.builder() - .adjustForBidAdjustment(false) - .build()) - .build()) - .build(); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - givenBidRequest(identity()), - account); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldNotApplyFactorIfAdjustmentDisabledByRequest() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(givenMediaTypes(Map.of( - ImpMediaType.video, - Map.of(RUBICON, BigDecimal.valueOf(0.85D))))) - .build()) - .floors(PriceFloorRules.builder() - .enforcement(PriceFloorEnforcement.builder() - .bidAdjustment(false) - .build()) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldApplyNoAdjustmentsIfBidAdjustmentsFactorIsNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustmentfactors(null).build()))); - final Imp imp = givenImp(identity()); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - imp, - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", imp.getBidfloor())); - } - - @Test - public void revertAdjustmentForImpShouldReturnNullIfImpBidFloorIsNotPresent() { - // given - final Imp imp = givenImp(impBuilder -> impBuilder.bidfloor(null)); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - imp, - RUBICON, - givenBidRequest(identity()), - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", null)); - } - - @Test - public void revertAdjustmentForImpShouldReturnBidFloorNotFactoredByOtherBidder() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(givenMediaTypes(Map.of( - ImpMediaType.video, - Map.of("bidder", BigDecimal.valueOf(0.8D))))) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldReturnFactoredOfOneIfExtBidAdjustmentsFactorMediaTypesIsNull() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(null) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldReturnFactorOfOneIfNoMediaTypeInImpression() { - // given - final BidRequest bidRequest = givenBidRequest(identity()); - final Imp imp = givenImp(impBuilder -> impBuilder.video(null)); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - imp, - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldSkipMediaTypeIfNoMediaTypesOfImpFound() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(givenMediaTypes(Map.of( - ImpMediaType.video_outstream, - Map.of(RUBICON, BigDecimal.valueOf(0.8D))))) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( + final Price adjustedBidPrice = target.adjustForImp( givenImp(identity()), RUBICON, - bidRequest, - null); + givenBidRequest, + null, + new ArrayList<>()); // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); + assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", new BigDecimal(100))); } private static BidRequest givenBidRequest(UnaryOperator requestCustomizer) { diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java index 724aef1f391..ad4fdbe0470 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java @@ -33,6 +33,7 @@ import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Collections.emptyList; @@ -45,7 +46,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -55,8 +55,6 @@ public class BasicPriceFloorEnforcerTest { private BidRejectionTracker rejectionTracker; @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; - @Mock(strictness = LENIENT) - private PriceFloorAdjuster priceFloorAdjuster; @Mock private Metrics metrics; @@ -64,11 +62,10 @@ public class BasicPriceFloorEnforcerTest { @BeforeEach public void setUp() { - given(priceFloorAdjuster.revertAdjustmentForImp(any(), any(), any(), any())).willAnswer(invocation -> { - final Imp argument = invocation.getArgument(0); - return Price.of(argument.getBidfloorcur(), argument.getBidfloor()); - }); - priceFloorEnforcer = new BasicPriceFloorEnforcer(currencyConversionService, priceFloorAdjuster, metrics); + given(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + + priceFloorEnforcer = new BasicPriceFloorEnforcer(currencyConversionService, metrics); } @Test @@ -320,33 +317,24 @@ public void shouldRejectBidsHavingPriceBelowFloor() { request.imp(givenImps(imp -> imp.id("impId1"), imp -> imp.id("impId2"))).cur(singletonList("USD"))); final BidRequest bidderBidRequest = givenBidRequest(request -> - request.imp(givenImps( - imp -> imp.id("impId1").bidfloor(new BigDecimal("0.11")).bidfloorcur("USD"), - imp -> imp.id("impId2").bidfloor(new BigDecimal("0.22")).bidfloorcur("USD")))); + request.imp(givenImps(imp -> imp.id("impId1"), imp -> imp.id("impId2")))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidderBidRequest, givenBidderSeatBid( bid -> bid.id("bidId1").impid("impId1").price(BigDecimal.ONE), - bid -> bid.id("bidId2").impid("impId2").price(BigDecimal.TEN))); + bid -> bid.id("bidId2").impid("impId2").price(BigDecimal.TEN)), + Map.of( + "impId1", Price.of("USD", new BigDecimal("1.1")), + "impId2", Price.of("USD", new BigDecimal("2.2")))); final Account account = givenAccount(identity()); - final String givenBidder = auctionParticipation.getBidderResponse().getBidder(); - final List givenImps = bidderBidRequest.getImp(); - - given(priceFloorAdjuster.revertAdjustmentForImp(givenImps.get(0), givenBidder, bidRequest, account)) - .willReturn(Price.of("USD", new BigDecimal("1.1"))); - given(priceFloorAdjuster.revertAdjustmentForImp(givenImps.get(1), givenBidder, bidRequest, account)) - .willReturn(Price.of("USD", new BigDecimal("2.2"))); - // when final AuctionParticipation result = priceFloorEnforcer.enforce( bidRequest, auctionParticipation, account, rejectionTracker); // then - verify(priceFloorAdjuster, times(2)).revertAdjustmentForImp(any(), any(), any(), any()); - final BidderBid rejectedBid = BidderBid.of( Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), null, null); verify(rejectionTracker).rejectBid(rejectedBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); @@ -368,12 +356,13 @@ public void shouldRejectBidsHavingPriceBelowFloor() { public void shouldRejectBidsHavingPriceBelowFloorAndRequestEnforceFloorsRateIs100() { // given final BidRequest bidRequest = givenBidRequest( - request -> request.imp(givenImps(imp -> imp.bidfloor(BigDecimal.TEN))), + request -> request.imp(givenImps(identity())), enforcement -> enforcement.enforceRate(100)); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.ONE))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.ONE)), + Map.of("impId", Price.of("USD", BigDecimal.TEN))); final Account account = givenAccount(identity()); @@ -495,12 +484,13 @@ public void shouldRemainBidsEvenCurrencyConversionForFloorIsFailed() { .willThrow(new PreBidException("error")); final BidRequest bidRequest = givenBidRequest(request -> request - .imp(givenImps(imp -> imp.bidfloorcur("USD").bidfloor(BigDecimal.ONE))) + .imp(givenImps(identity())) .cur(singletonList("EUR"))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN)), + Map.of("impId", Price.of("USD", BigDecimal.ONE))); final Account account = givenAccount(identity()); @@ -528,12 +518,13 @@ public void shouldRemainBidsHavingPriceGreaterThenConvertedFloor() { given(currencyConversionService.convertCurrency(any(), any(), any(), any())).willReturn(BigDecimal.TEN); final BidRequest bidRequest = givenBidRequest(request -> request - .imp(givenImps(imp -> imp.bidfloorcur("USD").bidfloor(BigDecimal.ONE))) + .imp(givenImps(identity())) .cur(singletonList("EUR"))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN)), + Map.of("impId", Price.of("USD", BigDecimal.ONE))); final Account account = givenAccount(identity()); @@ -558,14 +549,15 @@ public void shouldRemainBidsHavingPriceGreaterThenConvertedFloorInAnotherCurrenc given(currencyConversionService.convertCurrency(any(), any(), any(), any())).willReturn(BigDecimal.TEN); final BidRequest bidRequest = givenBidRequest(request -> request - .imp(givenImps(imp -> imp.bidfloor(BigDecimal.ONE).bidfloorcur("JPY"))) + .imp(givenImps(identity())) .cur(singletonList("EUR"))); final BidRequest bidderRequest = givenBidRequest(request -> bidRequest.toBuilder().cur(singletonList("USD"))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidderRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN)), + Map.of("impId", Price.of("JPY", BigDecimal.ONE))); final Account account = givenAccount(identity()); @@ -649,16 +641,24 @@ public void shouldRemainBidsHavingPriceGreaterThenConvertedCustomBidderFloorInAn .hasSize(1); } - private static AuctionParticipation givenAuctionParticipation(BidRequest bidRequest, BidderSeatBid bidderSeatBid) { + private static AuctionParticipation givenAuctionParticipation(BidRequest bidRequest, + BidderSeatBid bidderSeatBid, + Map priceFloors) { + return AuctionParticipation.builder() .bidderRequest(BidderRequest.builder() .bidder("bidder1") .bidRequest(bidRequest) + .originalPriceFloors(priceFloors) .build()) .bidderResponse(BidderResponse.of("bidder", bidderSeatBid, 0)) .build(); } + private static AuctionParticipation givenAuctionParticipation(BidRequest bidRequest, BidderSeatBid bidderSeatBid) { + return givenAuctionParticipation(bidRequest, bidderSeatBid, null); + } + private static Account givenAccount( UnaryOperator accountFloorsCustomizer) { diff --git a/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java b/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java index db15a1c8c1e..7dd8fdf575a 100644 --- a/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java +++ b/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java @@ -516,25 +516,6 @@ public void adjustForImpShouldCallDelegateWhenModelHasBidderSetAndTakesPrecenden verify(delegate).adjustForImp(givenImp, "bidder", givenBidRequest, givenAccount, debugWarnings); } - @Test - public void revertAdjustmentForImpShouldAlwaysAndOnlyCallDelegate() { - // given - final BidRequest givenBidRequest = BidRequest.builder().build(); - final Imp givenImp = givenImp(); - final Account givenAccount = Account.builder().build(); - - final Price expectedPrice = Price.of("EUR", BigDecimal.ONE); - - given(delegate.revertAdjustmentForImp(givenImp, "bidder", givenBidRequest, givenAccount)) - .willReturn(expectedPrice); - - // when - final Price actual = target.revertAdjustmentForImp(givenImp, "bidder", givenBidRequest, givenAccount); - - // then - assertThat(actual).isSameAs(expectedPrice); - } - private static BidRequest givenBidRequest(List modelGroupBidders, List dataBidders, List enforcementBidders) { From c26640f271d68c6b5b439c16a8adefb1680854da Mon Sep 17 00:00:00 2001 From: prebid-startio Date: Tue, 10 Jun 2025 15:41:43 +0300 Subject: [PATCH 21/51] New Adapter: Start.io (#3941) --- .../server/bidder/startio/StartioBidder.java | 149 ++++++++ .../bidder/StartioBidderConfiguration.java | 41 +++ src/main/resources/bidder-config/startio.yaml | 14 + .../static/bidder-params/startio.json | 8 + .../bidder/startio/StartioBidderTest.java | 318 ++++++++++++++++++ .../org/prebid/server/it/StartioTest.java | 35 ++ .../startio/test-auction-startio-request.json | 187 ++++++++++ .../test-auction-startio-response.json | 51 +++ .../startio/test-startio-bid-request.json | 196 +++++++++++ .../startio/test-startio-bid-response.json | 37 ++ .../server/it/test-application.properties | 2 + 11 files changed, 1038 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/startio/StartioBidder.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/StartioBidderConfiguration.java create mode 100644 src/main/resources/bidder-config/startio.yaml create mode 100644 src/main/resources/static/bidder-params/startio.json create mode 100644 src/test/java/org/prebid/server/bidder/startio/StartioBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/StartioTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/startio/StartioBidder.java b/src/main/java/org/prebid/server/bidder/startio/StartioBidder.java new file mode 100644 index 00000000000..e7ce856a4bc --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/startio/StartioBidder.java @@ -0,0 +1,149 @@ +package org.prebid.server.bidder.startio; + +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import 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.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class StartioBidder implements Bidder { + + private static final JsonPointer BID_TYPE_POINTER = JsonPointer.valueOf("/prebid/type"); + private static final String BID_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public StartioBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + if (hasNoAppOrSiteId(bidRequest)) { + return Result.withError(BidderError.badInput( + "Bidder requires either app.id or site.id to be specified.")); + } + + if (isSupportedCurrency(bidRequest.getCur())) { + return Result.withError(BidderError.badInput("Unsupported currency, bidder only accepts USD.")); + } + + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final List imps = bidRequest.getImp(); + for (int i = 0; i < imps.size(); i++) { + final Imp imp = imps.get(i); + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { + errors.add(BidderError.badInput( + "imp[%d]: Unsupported media type, bidder does not support audio.".formatted(i))); + + continue; + } + + final Imp modifiedImp = imp.getAudio() != null + ? imp.toBuilder().audio(null).build() + : imp; + requests.add(BidderUtil.defaultRequest( + bidRequest.toBuilder().imp(Collections.singletonList(modifiedImp)).build(), + endpointUrl, mapper)); + } + + return Result.of(requests, errors); + } + + private static boolean hasNoAppOrSiteId(BidRequest bidRequest) { + final App app = bidRequest.getApp(); + final Site site = bidRequest.getSite(); + return (app == null || StringUtils.isEmpty(app.getId())) + && (site == null || StringUtils.isEmpty(site.getId())); + } + + private static boolean isSupportedCurrency(List currencies) { + return CollectionUtils.isNotEmpty(currencies) && !currencies.contains(BID_CURRENCY); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse response; + + try { + response = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + return Result.of(extractBids(response, errors), errors); + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> constructBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid constructBidderBid(Bid bid, String currency, List errors) { + final BidType type = getBidType(bid); + if (type != null) { + return BidderBid.of(bid, type, currency); + } + + errors.add(BidderError.badServerResponse( + "Failed to parse bid media type for impression %s.".formatted(bid.getImpid()))); + return null; + } + + private static BidType getBidType(Bid bid) { + final JsonNode ext = bid.getExt(); + final JsonNode bidType = ext != null ? ext.at(BID_TYPE_POINTER) : null; + if (bidType == null || !bidType.isTextual()) { + return null; + } + + return switch (bidType.textValue()) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + case "native" -> BidType.xNative; + default -> null; + }; + } + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/StartioBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/StartioBidderConfiguration.java new file mode 100644 index 00000000000..1cb3bda1cf3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/StartioBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.startio.StartioBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/startio.yaml", factory = YamlPropertySourceFactory.class) +public class StartioBidderConfiguration { + + private static final String BIDDER_NAME = "startio"; + + @Bean("startioConfigurationProperties") + @ConfigurationProperties("adapters.startio") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps startioBidderDeps(BidderConfigurationProperties startioConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(startioConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new StartioBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/startio.yaml b/src/main/resources/bidder-config/startio.yaml new file mode 100644 index 00000000000..50be8e27fc2 --- /dev/null +++ b/src/main/resources/bidder-config/startio.yaml @@ -0,0 +1,14 @@ +adapters: + startio: + endpoint: http://pbs-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbs + meta-info: + maintainer-email: prebid@start.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + vendor-id: 1216 diff --git a/src/main/resources/static/bidder-params/startio.json b/src/main/resources/static/bidder-params/startio.json new file mode 100644 index 00000000000..bae19ac4e81 --- /dev/null +++ b/src/main/resources/static/bidder-params/startio.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Start.io Adapter Params", + "description": "A schema which validates params accepted by the Start.io adapter", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/src/test/java/org/prebid/server/bidder/startio/StartioBidderTest.java b/src/test/java/org/prebid/server/bidder/startio/StartioBidderTest.java new file mode 100644 index 00000000000..31a7c4ce91f --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/startio/StartioBidderTest.java @@ -0,0 +1,318 @@ +package org.prebid.server.bidder.startio; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Audio; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.vertx.core.MultiMap; +import org.junit.jupiter.api.BeforeEach; +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.response.BidType; +import org.prebid.server.util.HttpUtil; + +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; + +public class StartioBidderTest extends VertxTest { + + private StartioBidder target; + + @BeforeEach + public void setUp() { + target = new StartioBidder("http://test.endpoint.com", jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy( + () -> new StartioBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCorrectlyAddHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getHeaders) + .flatExtracting(MultiMap::entries) + .extracting(Map.Entry::getKey, Map.Entry::getValue) + .containsExactlyInAnyOrder( + tuple(HttpUtil.CONTENT_TYPE_HEADER.toString(), HttpUtil.APPLICATION_JSON_CONTENT_TYPE), + tuple(HttpUtil.ACCEPT_HEADER.toString(), HttpHeaderValues.APPLICATION_JSON.toString())); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfNoAppOrSiteSpecified() { + // given + final BidRequest bidRequest = givenBidRequest( + requestCustomizer -> requestCustomizer.app(null).site(null), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).contains( + BidderError.badInput("Bidder requires either app.id or site.id to be specified.")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfNoAppOrSiteIdSpecified() { + // given + final BidRequest bidRequest = givenBidRequest( + requestCustomizer -> requestCustomizer + .app(App.builder().id(null).build()) + .site(Site.builder().id(null).build()), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).contains( + BidderError.badInput("Bidder requires either app.id or site.id to be specified.")); + } + + @Test + public void makeHttpRequestsShouldReturnRequestIfSiteIdPresent() { + // given + final BidRequest bidRequest = givenBidRequest( + requestCustomizer -> requestCustomizer.app(null) + .site(Site.builder().id("siteId").build()), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(0); + assertThat(result.getValue()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldReturnErrorForUnsupportedCurrency() { + // given + final BidRequest bidRequest = givenBidRequest( + requestCustomizer -> requestCustomizer.cur(List.of("EUR")), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).contains( + BidderError.badInput("Unsupported currency, bidder only accepts USD.")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorWhenImpressionContainsOnlyAudio() { + // given + final Audio audio = Audio.builder().mimes(List.of("audio/mp3")).build(); + final BidRequest bidRequest = givenBidRequest(identity(), + impBuilder -> impBuilder.id("Imp01").banner(null).video(null).xNative(null).audio(audio)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1).contains( + BidderError.badInput("imp[0]: Unsupported media type, bidder does not support audio.")); + } + + @Test + public void makeHttpRequestsShouldRemoveAudioFromImpressionIfItContainsMultipleMediaTypes() { + // given + final Audio audio = Audio.builder().mimes(List.of("audio/mp3")).build(); + final Banner banner = Banner.builder().build(); + final BidRequest bidRequest = givenBidRequest(identity(), + impBuilder -> impBuilder.id("Imp01").audio(audio).banner(banner)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .allSatisfy(impression -> { + assertThat(impression.getAudio()).isEqualTo(null); + assertThat(impression.getBanner()).isEqualTo(banner); + }); + } + + @Test + public void makeHttpRequestsShouldSplitRequestByImpressions() { + // given + final Banner banner = Banner.builder().w(100).h(100).build(); + final Video video = Video.builder().w(100).h(200).build(); + final Native nativeImp = Native.builder().request("request").build(); + final Imp imp1 = givenImp(impBuilder -> impBuilder.id("imp1").banner(banner)); + final Imp imp2 = givenImp(impBuilder -> impBuilder.id("imp2").video(video)); + final Imp imp3 = givenImp(impBuilder -> impBuilder.id("imp3").xNative(nativeImp)); + final BidRequest bidRequest = givenBidRequest( + requestCustomizer -> requestCustomizer.imp(List.of(imp1, imp2, imp3)), + identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(3) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getImp) + .allSatisfy(imps -> assertThat(imps).hasSize(1)) + .extracting(List::getFirst) + .satisfiesExactlyInAnyOrder( + impression -> assertThat(impression).isEqualTo(imp1), + impression -> assertThat(impression).isEqualTo(imp2), + impression -> assertThat(impression).isEqualTo(imp3)); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall(null, "invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReportErrorAndSkipBidIfCannotParseBidType() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(null, + givenBidResponse( + givenBid("001", BidType.banner), + givenBid("002", BidType.video), + givenBid("003", BidType.xNative), + givenBid("004", BidType.audio), + givenBid("005", BidType.banner).toBuilder().ext(null).build(), + givenBid("006", BidType.banner).toBuilder().ext( + mapper.createObjectNode().put("prebid", false)).build(), + givenBid("007", BidType.banner).toBuilder().ext( + mapper.createObjectNode().set("prebid", + mapper.createObjectNode().put("type", "not a banner"))).build())); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(4).allSatisfy( + error -> assertThat( + error.getMessage()).startsWith("Failed to parse bid media type for impression")); + assertThat(result.getValue()).hasSize(3).allSatisfy( + bid -> assertThat(bid.getBid().getImpid()).isIn(List.of("001", "002", "003"))); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), impCustomizer); + } + + private static BidRequest givenBidRequest( + UnaryOperator bidRequestCustomizer, + UnaryOperator impCustomizer) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .app(App.builder().id("appId").build()) + .imp(singletonList(givenImp(impCustomizer)))) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("345").banner(Banner.builder().build())).build(); + } + + private static BidderCall givenHttpCall(BidRequest bidRequest, String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } + + private BidderCall givenHttpCall(BidRequest bidRequest, BidResponse bidResponse) + throws JsonProcessingException { + return givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponse)); + } + + private static BidResponse givenBidResponse(Bid... bids) { + return BidResponse.builder().seatbid(singletonList( + SeatBid.builder().bid(asList(bids)).build())).build(); + } + + private static Bid givenBid(String impid, BidType bidType) { + return Bid.builder() + .impid(impid) + .ext(mapper.createObjectNode() + .set("prebid", mapper.createObjectNode() + .put("type", bidType.getName()))) + .build(); + } + +} diff --git a/src/test/java/org/prebid/server/it/StartioTest.java b/src/test/java/org/prebid/server/it/StartioTest.java new file mode 100644 index 00000000000..217b9ebfc2b --- /dev/null +++ b/src/test/java/org/prebid/server/it/StartioTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.prebid.server.model.Endpoint; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +@RunWith(SpringRunner.class) +public class StartioTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromStartio() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/startio-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/startio/test-startio-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/startio/test-startio-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/startio/test-auction-startio-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/startio/test-auction-startio-response.json", response, singletonList("startio")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-request.json b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-request.json new file mode 100644 index 00000000000..5e659da50de --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-request.json @@ -0,0 +1,187 @@ +{ + "id": "123456789", + "at": 1, + "tmax": 485, + "imp": [ + { + "id": "123456789", + "tagid": "999999-1111-8888-2222-77777777777", + "instl": 1, + "bidfloor": 0.5, + "bidfloorcur": "USD", + "video": { + "api": [ + 7 + ], + "battr": [ + 16 + ], + "companiontype": [ + 1, + 2 + ], + "h": 480, + "w": 320, + "skip": 1, + "mimes": [ + "video/mp4" + ], + "minduration": 5, + "maxduration": 30, + "linearity": 1, + "protocols": [ + 2, + 3, + 5, + 6 + ], + "pos": 7, + "startdelay": 0, + "placement": 4, + "skipmin": 0, + "skipafter": 0, + "companionad": [ + { + "w": 320, + "h": 480, + "pos": 7, + "mimes": [ + "image/jpg", + "image/gif" + ], + "api": [ + 3, + 5, + 6 + ], + "vcm": 1 + } + ], + "delivery": [], + "maxextended": 0, + "ext": { + "rewarded": 1, + "videotype": "rewarded", + "plcmt": 3 + }, + "plcmt": 2 + }, + "metric": [ + { + "type": "viewability", + "value": 0.97, + "vendor": "ZZZ" + } + ], + "displaymanager": "ZZZ", + "displaymanagerver": "42.42.42", + "secure": 1, + "ext": { + "prebid": { + "bidder": { + "startio": {} + } + } + } + } + ], + "app": { + "id": "aaaaaa-ffff-bbbb-eeee-cccccccccccc", + "name": "appname", + "domain": "pubname.com", + "privacypolicy": 1, + "publisher": { + "id": "192837465", + "name": "pubname", + "domain": "pubname.com" + }, + "bundle": "com.pubname.appname", + "content": { + "userrating": "4.53" + }, + "storeurl": "https://play.google.com/store/apps/details?id=com.pubname.appname", + "ver": "2.27.2", + "paid": 0 + }, + "device": { + "ip": "111.111.111.111", + "devicetype": 4, + "carrier": "Verizon ", + "connectiontype": 6, + "make": "samsung", + "model": "SM-G960U", + "ua": "Mozilla/5.0 (Linux; Android 10; SM-G960U Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.39 Mobile Safari/537.36", + "os": "Android", + "osv": "10", + "geo": { + "country": "USA", + "city": "Pennsauken", + "type": 2, + "region": "NJ", + "zip": "08110", + "metro": "504", + "ipservice": 3 + }, + "ifa": "11111111-2222-9999-8888-3333333333", + "h": 740, + "w": 360, + "pxratio": 2, + "lmt": 0, + "language": "en" + }, + "cur": [ + "USD" + ], + "bcat": [ + "IAB23-1", + "IAB7-26", + "IAB7-44", + "IAB7-25", + "IAB7-30", + "IAB23-5", + "IAB25-3", + "IAB26-2", + "IAB23-4", + "IAB25-2", + "IAB26-1", + "IAB23-3", + "IAB25-1", + "IAB23-2", + "IAB23-9", + "IAB25-7", + "IAB23-8", + "IAB25-6", + "IAB23-7", + "IAB25-5", + "IAB26-4", + "IAB23-6", + "IAB25-4", + "IAB26-3", + "IAB23-10", + "IAB7-39", + "IAB7-5", + "IAB1-8", + "IAB11-5", + "IAB15-1", + "IAB14-1", + "IAB11-4", + "IAB15-5", + "IAB18-2", + "IAB9-7" + ], + "badv": [ + "domain1.com", + "domain2.com", + "domain3.com", + "domain4.com" + ], + "source": { + "ext": { + "omidpn": "omidpn", + "omidpv": "omidpv" + } + }, + "ext": { + "hb": 1 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-response.json new file mode 100644 index 00000000000..6d1805379ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-auction-startio-response.json @@ -0,0 +1,51 @@ +{ + "id": "123456789", + "seatbid": [ + { + "bid": [ + { + "id": "0213b3e3-c59a-4129-aba5-458ad77b2d30", + "impid": "123456789", + "price": 0.6018685301576561, + "nurl": "https://adwin.startappservice.com/adwin/api/v1.0/adwin?d=dparam", + "lurl": "https://img.image.com/product/image.jpg", + "adm": "\n \n \n iabtechlab\n http://example.com/error\n \n \n \n \n \n \n \n http://example.com/track/impression\n \n \n \n iabtechlab video ad\n AD CONTENT description category\n \n \n 8465\n \n \n http://example.com/tracking/start\n http://example.com/tracking/firstQuartile\n http://example.com/tracking/midpoint\n http://example.com/tracking/thirdQuartile\n http://example.com/tracking/complete\n http://example.com/tracking/progress-10\n \n 00:00:16\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "adomain": [ + "start.io" + ], + "iurl": "https://adimpression.startappservice.com/adimpression/api/v1.0/adimp?d=dparam", + "cid": "9147870261277510647", + "crid": "4176712578365726823", + "cat": [ + "IAB0" + ], + "w": 480, + "h": 320, + "exp": 1500, + "ext": { + "origbidcpm": 0.6018685301576561, + "origbidcur": "USD", + "prebid": { + "type": "video", + "meta": { + "adaptercode": "startio" + } + } + } + } + ], + "seat": "startio", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "startio": "{{ startio.response_time_ms }}" + }, + "tmaxrequest": 485, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-request.json new file mode 100644 index 00000000000..d62a63548d4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-request.json @@ -0,0 +1,196 @@ +{ + "id": "123456789", + "imp": [ + { + "id": "123456789", + "metric": [ + { + "type": "viewability", + "value": 0.97, + "vendor": "ZZZ" + } + ], + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 5, + "maxduration": 30, + "startdelay": 0, + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 320, + "h": 480, + "placement": 4, + "plcmt": 2, + "linearity": 1, + "skip": 1, + "skipmin": 0, + "skipafter": 0, + "battr": [ + 16 + ], + "maxextended": 0, + "delivery": [], + "pos": 7, + "companionad": [ + { + "w": 320, + "h": 480, + "pos": 7, + "mimes": [ + "image/jpg", + "image/gif" + ], + "api": [ + 3, + 5, + 6 + ], + "vcm": 1 + } + ], + "api": [ + 7 + ], + "companiontype": [ + 1, + 2 + ], + "ext": { + "rewarded": 1, + "videotype": "rewarded", + "plcmt": 3 + } + }, + "displaymanager": "ZZZ", + "displaymanagerver": "42.42.42", + "instl": 1, + "tagid": "999999-1111-8888-2222-77777777777", + "bidfloor": 0.5, + "bidfloorcur": "USD", + "secure": 1, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": {} + } + } + ], + "app": { + "id": "aaaaaa-ffff-bbbb-eeee-cccccccccccc", + "name": "appname", + "bundle": "com.pubname.appname", + "domain": "pubname.com", + "storeurl": "https://play.google.com/store/apps/details?id=com.pubname.appname", + "ver": "2.27.2", + "privacypolicy": 1, + "paid": 0, + "publisher": { + "id": "192837465", + "name": "pubname", + "domain": "pubname.com" + }, + "content": { + "userrating": "4.53" + } + }, + "device": { + "geo": { + "type": 2, + "ipservice": 3, + "country": "USA", + "region": "NJ", + "metro": "504", + "city": "Pennsauken", + "zip": "08110" + }, + "lmt": 0, + "ua": "Mozilla/5.0 (Linux; Android 10; SM-G960U Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.39 Mobile Safari/537.36", + "ip": "111.111.111.111", + "devicetype": 4, + "make": "samsung", + "model": "SM-G960U", + "os": "Android", + "osv": "10", + "h": 740, + "w": 360, + "pxratio": 2, + "language": "en", + "carrier": "Verizon ", + "connectiontype": 6, + "ifa": "11111111-2222-9999-8888-3333333333" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "bcat": [ + "IAB23-1", + "IAB7-26", + "IAB7-44", + "IAB7-25", + "IAB7-30", + "IAB23-5", + "IAB25-3", + "IAB26-2", + "IAB23-4", + "IAB25-2", + "IAB26-1", + "IAB23-3", + "IAB25-1", + "IAB23-2", + "IAB23-9", + "IAB25-7", + "IAB23-8", + "IAB25-6", + "IAB23-7", + "IAB25-5", + "IAB26-4", + "IAB23-6", + "IAB25-4", + "IAB26-3", + "IAB23-10", + "IAB7-39", + "IAB7-5", + "IAB1-8", + "IAB11-5", + "IAB15-1", + "IAB14-1", + "IAB11-4", + "IAB15-5", + "IAB18-2", + "IAB9-7" + ], + "badv": [ + "domain1.com", + "domain2.com", + "domain3.com", + "domain4.com" + ], + "source": { + "tid": "${json-unit.any-string}", + "ext": { + "omidpv": "omidpv", + "omidpn": "omidpn" + } + }, + "ext": { + "prebid": { + "channel": { + "name": "app" + }, + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + }, + "hb": 1 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-response.json new file mode 100644 index 00000000000..dac8b1ab26b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/startio/test-startio-bid-response.json @@ -0,0 +1,37 @@ +{ + "id": "123456789", + "seatbid": [ + { + "bid": [ + { + "id": "0213b3e3-c59a-4129-aba5-458ad77b2d30", + "impid": "123456789", + "price": 0.6018685301576561, + "adm": "\n \n \n iabtechlab\n http://example.com/error\n \n \n \n \n \n \n \n http://example.com/track/impression\n \n \n \n iabtechlab video ad\n AD CONTENT description category\n \n \n 8465\n \n \n http://example.com/tracking/start\n http://example.com/tracking/firstQuartile\n http://example.com/tracking/midpoint\n http://example.com/tracking/thirdQuartile\n http://example.com/tracking/complete\n http://example.com/tracking/progress-10\n \n 00:00:16\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "nurl": "https://adwin.startappservice.com/adwin/api/v1.0/adwin?d=dparam", + "lurl": "https://img.image.com/product/image.jpg", + "iurl": "https://adimpression.startappservice.com/adimpression/api/v1.0/adimp?d=dparam", + "adomain": [ + "start.io" + ], + "cid": "9147870261277510647", + "crid": "4176712578365726823", + "w": 480, + "h": 320, + "cat": [ + "IAB0" + ], + "ext": { + "prebid":{ + "type": "video" + } + } + } + ], + "seat": "start.io", + "group": 0 + } + ], + "cur": "USD", + "bidid": "e3c853f1-fc21-42bb-8896-59fed5564788" +} 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 d063f6c1da4..717b0c09445 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -494,6 +494,8 @@ adapters.silvermob.enabled=true adapters.silvermob.endpoint=http://localhost:8090/silvermob-exchange adapters.silverpush.enabled=true adapters.silverpush.endpoint=http://localhost:8090/silverpush-exchange +adapters.startio.enabled=true +adapters.startio.endpoint=http://localhost:8090/startio-exchange adapters.stroeercore..enabled=true adapters.stroeercore.endpoint=http://localhost:8090/stroeercore-exchange adapters.suntContent.enabled=true From e22f78117b03ad9bd8b007b08d89dd8d2ca3471b Mon Sep 17 00:00:00 2001 From: Yuriy Velichko Date: Tue, 10 Jun 2025 15:42:29 +0300 Subject: [PATCH 22/51] Update README.md - Add the required JAVA SDK version (#4003) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9fbfe912715..b5aa6539aae 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Follow next steps to create JAR file which can be deployed locally. - Install prerequsites - Java SDK: Oracle's or Corretto. Let us know if there's a distribution PBS-Java doesn't work with. + - Java SDK Version: 21 - Maven - Clone the project: From 9a2fead0533cfbad5dce01f845f20f10bb23de82 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:43:08 +0200 Subject: [PATCH 23/51] Cache endpoint split for response (#3981) --- docs/config-app.md | 3 + .../prebid/server/cache/CoreCacheService.java | 28 +-- .../spring/config/ServiceConfiguration.java | 57 ++++++- .../server/functional/tests/CacheSpec.groovy | 160 +++++++++++++++++- .../server/cache/CoreCacheServiceTest.java | 117 +++++++++++++ 5 files changed, 344 insertions(+), 21 deletions(-) diff --git a/docs/config-app.md b/docs/config-app.md index 5628341a812..ca0f53e51a0 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -284,6 +284,9 @@ For `JVM` metrics - `cache.scheme` - set the external Cache Service protocol: `http`, `https`, etc. - `cache.host` - set the external Cache Service destination in format `host:port`. - `cache.path` - set the external Cache Service path, for example `/cache`. +- `cache.internal.scheme` - set the internal Cache Service protocol: `http`, `https`, etc., the internal scheme get priority over the external one when provided. +- `cache.internal.host` - set the internal Cache Service destination in format `host:port`, the internal port get priority over the external one when provided. +- `cache.internal.path` - set the internal Cache Service path, for example `/cache`, the internal path get priority over the external one when provided. - `storage.pbc.enabled` - If set to true, this will allow storing modules’ data in third-party storage. - `storage.pbc.path` - set the external Cache Service path for module caching, for example `/pbc-storage`. - `cache.api-key-secured` - if set to `true`, will cause Prebid Server to add a special API key header to Prebid Cache requests. diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java index d3a8b198b24..863c25ee38b 100644 --- a/src/main/java/org/prebid/server/cache/CoreCacheService.java +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -68,7 +68,8 @@ public class CoreCacheService { private static final int MAX_DATACENTER_REGION_LENGTH = 4; private final HttpClient httpClient; - private final URL endpointUrl; + private final URL externalEndpointUrl; + private final URL internalEndpointUrl; private final String cachedAssetUrlTemplate; private final long expectedCacheTimeMs; private final VastModifier vastModifier; @@ -86,7 +87,8 @@ public class CoreCacheService { public CoreCacheService( HttpClient httpClient, - URL endpointUrl, + URL externalEndpointUrl, + URL internalEndpointUrl, String cachedAssetUrlTemplate, long expectedCacheTimeMs, String apiKey, @@ -101,7 +103,8 @@ public CoreCacheService( JacksonMapper mapper) { this.httpClient = Objects.requireNonNull(httpClient); - this.endpointUrl = Objects.requireNonNull(endpointUrl); + this.externalEndpointUrl = Objects.requireNonNull(externalEndpointUrl); + this.internalEndpointUrl = internalEndpointUrl; this.cachedAssetUrlTemplate = Objects.requireNonNull(cachedAssetUrlTemplate); this.expectedCacheTimeMs = expectedCacheTimeMs; this.vastModifier = Objects.requireNonNull(vastModifier); @@ -121,13 +124,13 @@ public CoreCacheService( } public String getEndpointHost() { - final String host = endpointUrl.getHost(); - final int port = endpointUrl.getPort(); + final String host = externalEndpointUrl.getHost(); + final int port = externalEndpointUrl.getPort(); return port != -1 ? "%s:%d".formatted(host, port) : host; } public String getEndpointPath() { - return endpointUrl.getPath(); + return externalEndpointUrl.getPath(); } public String getCachedAssetURLTemplate() { @@ -142,7 +145,7 @@ public String cacheVideoDebugLog(CachedDebugLog cachedDebugLog, Integer videoCac makeDebugCacheCreative(cachedDebugLog, cacheKey, videoCacheTtl)); final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives); httpClient.post( - endpointUrl.toString(), + ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl).toString(), cacheHeaders, mapper.encodeToString(bidCacheRequest), expectedCacheTimeMs); @@ -179,7 +182,7 @@ private Future makeRequest(BidCacheRequest bidCacheRequest, final long startTime = clock.millis(); return httpClient.post( - endpointUrl.toString(), + ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl).toString(), cacheHeaders, mapper.encodeToString(bidCacheRequest), remainingTimeout) @@ -308,9 +311,9 @@ private Future doCacheOpenrtb(List bids, updateCreativeMetrics(accountId, cachedCreatives); - final String url = endpointUrl.toString(); + final String url = ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl).toString(); final String body = mapper.encodeToString(bidCacheRequest); - final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body); + final CacheHttpRequest httpRequest = CacheHttpRequest.of(externalEndpointUrl.toString(), body); final long startTime = clock.millis(); return httpClient.post(url, cacheHeaders, body, remainingTimeout) @@ -336,7 +339,8 @@ private CacheServiceResult processResponseOpenrtb(HttpClientResponse response, final CacheHttpResponse httpResponse = CacheHttpResponse.of(response.getStatusCode(), response.getBody()); final int responseStatusCode = response.getStatusCode(); - final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), httpRequest, httpResponse, startTime); + final DebugHttpCall httpCall = makeDebugHttpCall( + externalEndpointUrl.toString(), httpRequest, httpResponse, startTime); final BidCacheResponse bidCacheResponse; try { bidCacheResponse = toBidCacheResponse( @@ -359,7 +363,7 @@ private CacheServiceResult failResponseOpenrtb(Throwable exception, metrics.updateCacheRequestFailedTime(accountId, clock.millis() - startTime); - final DebugHttpCall httpCall = makeDebugHttpCall(endpointUrl.toString(), request, null, startTime); + final DebugHttpCall httpCall = makeDebugHttpCall(externalEndpointUrl.toString(), request, null, startTime); return CacheServiceResult.of(httpCall, exception, Collections.emptyMap()); } 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 360d77b2bae..aa57b0b04b7 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -6,6 +6,7 @@ import io.vertx.core.file.FileSystem; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.net.JksOptions; +import lombok.Data; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.activity.ActivitiesConfigResolver; @@ -158,14 +159,9 @@ public class ServiceConfiguration { @Bean CoreCacheService cacheService( - @Value("${cache.scheme}") String scheme, - @Value("${cache.host}") String host, - @Value("${cache.path}") String path, - @Value("${cache.query}") String query, + CacheConfigurationProperties cacheConfigurationProperties, @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, @Value("${pbc.api.key:#{null}}") String apiKey, - @Value("${cache.api-key-secured:false}") boolean apiKeySecured, - @Value("${cache.append-trace-info-to-cache-id:false}") boolean appendTraceInfoToCacheId, @Value("${datacenter-region:#{null}}") String datacenterRegion, VastModifier vastModifier, EventsService eventsService, @@ -174,14 +170,25 @@ CoreCacheService cacheService( Clock clock, JacksonMapper mapper) { + final String scheme = cacheConfigurationProperties.getScheme(); + final String host = cacheConfigurationProperties.getHost(); + final String path = cacheConfigurationProperties.getPath(); + final String query = cacheConfigurationProperties.getQuery(); + final CacheConfigurationProperties.InternalCacheConfigurationProperties internalProperties = + cacheConfigurationProperties.getInternal(); + return new CoreCacheService( httpClient, CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), + internalProperties == null ? null : CacheServiceUtil.getCacheEndpointUrl( + internalProperties.getScheme(), + internalProperties.getHost(), + internalProperties.getPath()), CacheServiceUtil.getCachedAssetUrlTemplate(scheme, host, path, query), expectedCacheTimeMs, apiKey, - apiKeySecured, - appendTraceInfoToCacheId, + cacheConfigurationProperties.isApiKeySecured(), + cacheConfigurationProperties.isAppendTraceInfoToCacheId(), datacenterRegion, vastModifier, eventsService, @@ -191,6 +198,40 @@ CoreCacheService cacheService( mapper); } + @Bean + @ConfigurationProperties(prefix = "cache") + CacheConfigurationProperties cacheConfigurationProperties() { + return new CacheConfigurationProperties(); + } + + @Data + private static class CacheConfigurationProperties { + + private String scheme; + + private String host; + + private String path; + + private String query; + + boolean apiKeySecured; + + boolean appendTraceInfoToCacheId; + + private InternalCacheConfigurationProperties internal; + + @Data + private static class InternalCacheConfigurationProperties { + + private String scheme; + + private String host; + + private String path; + } + } + @Bean @ConditionalOnProperty(prefix = "cache.module", name = "enabled", havingValue = "false", matchIfMissing = true) PbcStorageService noOpModuleCacheService() { 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 2aec80a7e0a..b745ae53a1f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -15,16 +15,19 @@ import org.prebid.server.functional.model.response.auction.Adm import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.ErrorType.CACHE 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.response.auction.MediaType.BANNER import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class CacheSpec extends BaseSpec { private static final String PBS_API_HEADER = 'x-pbc-api-key' private static final Integer MAX_DATACENTER_REGION_LENGTH = 4 private static final Integer DEFAULT_UUID_LENGTH = 36 + private static final Integer TARGETING_PARAM_NAME_MAX_LENGTH = 20 private static final String XML_CREATIVE_SIZE_ACCOUNT_METRIC = "account.%s.prebid_cache.creative_size.xml" private static final String JSON_CREATIVE_SIZE_ACCOUNT_METRIC = "account.%s.prebid_cache.creative_size.json" @@ -36,6 +39,12 @@ class CacheSpec extends BaseSpec { private static final String JSON_CREATIVE_SIZE_GLOBAL_METRIC = "prebid_cache.creative_size.json" private static final String CACHE_REQUEST_OK_GLOBAL_METRIC = "prebid_cache.requests.ok" + private static final String CACHE_PATH = "/${PBSUtils.randomString}".toString() + private static final String CACHE_HOST = "${PBSUtils.randomString}:${PBSUtils.getRandomNumber(0, 65535)}".toString() + private static final String INTERNAL_CACHE_PATH = '/cache' + private static final String HTTP_SCHEME = 'http' + private static final String HTTPS_SCHEME = 'https' + 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, CACHE_REQUEST_OK_GLOBAL_METRIC) @@ -117,6 +126,7 @@ class CacheSpec extends BaseSpec { 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 pbsConfig = ['pbc.api.key': apiKey, 'cache.api-key-secured': 'false'] def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'false']) and: "Default BidRequest with cache, targeting" @@ -133,12 +143,16 @@ class CacheSpec extends BaseSpec { and: "PBS call shouldn't include api-key" assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } 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']) + def pbsConfig = ['pbc.api.key': apiKey, 'cache.api-key-secured': 'true'] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default BidRequest with cache, targeting" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -154,6 +168,9 @@ class CacheSpec extends BaseSpec { and: "PBS call should include api-key" assert prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] == [apiKey] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should cache banner bids with cache key that include account and datacenter short name when append-trace-info-to-cache-id enabled"() { @@ -502,6 +519,147 @@ class CacheSpec extends BaseSpec { " inline ${PBSUtils.getRandomString()} " | " ImpreSSion " } + def "PBS shouldn't cache bids when targeting is specified and config cache is invalid"() { + given: "Pbs config with cache" + def INVALID_PREBID_CACHE_CONFIG = ["cache.path" : CACHE_PATH, + "cache.scheme": HTTP_SCHEME, + "cache.host" : CACHE_HOST] + def pbsService = pbsServiceFactory.getService(INVALID_PREBID_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain error" + assert bidResponse.ext?.errors[CACHE]*.code == [999] + assert bidResponse.ext?.errors[CACHE]*.message[0] == ("Failed to resolve '${CACHE_HOST.tokenize(":")[0]}' [A(1)]") + + and: "Bid response targeting should contain value" + assert bidResponse.seatbid[0].bid[0].ext.prebid.targeting.findAll { it.key.startsWith("hb_cache") }.isEmpty() + + and: "PBS shouldn't call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 0 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(INVALID_PREBID_CACHE_CONFIG) + } + + def "PBS should cache bids and emit error when targeting is specified and config cache is valid and internal is invalid"() { + given: "Pbs config with cache" + def INVALID_PREBID_CACHE_CONFIG = ["cache.internal.path" : CACHE_PATH, + "cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : CACHE_HOST] + def pbsService = pbsServiceFactory.getService(INVALID_PREBID_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 0 + + and: "Seat bid shouldn't be discarded" + assert !bidResponse.seatbid.isEmpty() + + and: "Bid response targeting should contain value" + assert bidResponse.seatbid[0].bid[0].ext.prebid.targeting.findAll { it.key.startsWith("hb_cache") }.isEmpty() + + and: "Debug should contain http call with empty response body" + def cacheCall = bidResponse.ext.debug.httpcalls['cache'][0] + assert cacheCall.responseBody == null + assert cacheCall.uri == "${HTTP_SCHEME}://${networkServiceContainer.hostAndPort + INTERNAL_CACHE_PATH}" + + then: "Response should contain error" + assert bidResponse.ext?.errors[CACHE]*.code == [999] + assert bidResponse.ext?.errors[CACHE]*.message[0] == ("Failed to resolve '${CACHE_HOST.tokenize(":")[0]}' [A(1)]") + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(INVALID_PREBID_CACHE_CONFIG) + } + + def "PBS should cache bids when targeting is specified and config cache is invalid and internal cache config valid"() { + given: "Pbs config with cache" + def INVALID_PREBID_CACHE_CONFIG = ["cache.path" : CACHE_PATH, + "cache.scheme": HTTPS_SCHEME, + "cache.host" : CACHE_HOST,] + def VALID_INTERNAL_CACHE_CONFIG = ["cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : "$networkServiceContainer.hostAndPort".toString(), + "cache.internal.path" : INTERNAL_CACHE_PATH,] + def pbsService = pbsServiceFactory.getService(INVALID_PREBID_CACHE_CONFIG + VALID_INTERNAL_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "Bid response targeting should contain value" + verifyAll (bidResponse?.seatbid[0]?.bid[0]?.ext?.prebid?.targeting as Map) { + it.get("hb_cache_id") + it.get("hb_cache_id_generic") + it.get("hb_cache_path") == CACHE_PATH + it.get("hb_cache_host") == CACHE_HOST + it.get("hb_cache_path_generic".substring(0, TARGETING_PARAM_NAME_MAX_LENGTH)) == CACHE_PATH + it.get("hb_cache_host_generic".substring(0, TARGETING_PARAM_NAME_MAX_LENGTH)) == CACHE_HOST + } + + and: "Debug should contain http call" + assert bidResponse.ext.debug.httpcalls['cache'][0].uri == + "${HTTPS_SCHEME}://${CACHE_HOST + CACHE_PATH}" + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(INVALID_PREBID_CACHE_CONFIG + VALID_INTERNAL_CACHE_CONFIG) + } + + def "PBS should cache bids when targeting is specified and config cache and internal cache config valid"() { + given: "Pbs config with cache" + def VALID_INTERNAL_CACHE_CONFIG = ["cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : "$networkServiceContainer.hostAndPort".toString(), + "cache.internal.path" : INTERNAL_CACHE_PATH] + def pbsService = pbsServiceFactory.getService(VALID_INTERNAL_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "Bid response targeting should contain value" + verifyAll (bidResponse.seatbid[0].bid[0].ext.prebid.targeting) { + it.get("hb_cache_id") + it.get("hb_cache_id_generic") + it.get("hb_cache_path") == INTERNAL_CACHE_PATH + it.get("hb_cache_host") == networkServiceContainer.hostAndPort.toString() + it.get("hb_cache_path_generic".substring(0, TARGETING_PARAM_NAME_MAX_LENGTH)) == INTERNAL_CACHE_PATH + it.get("hb_cache_host_generic".substring(0, TARGETING_PARAM_NAME_MAX_LENGTH)) == networkServiceContainer.hostAndPort.toString() + } + + and: "Debug should contain http call" + assert bidResponse.ext.debug.httpcalls['cache'][0].uri == + "${HTTP_SCHEME}://${networkServiceContainer.hostAndPort + INTERNAL_CACHE_PATH}" + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(VALID_INTERNAL_CACHE_CONFIG) + } + def "PBS should cache bids and add targeting values when account cache config #accountAuctionConfig"() { given: "Current value of metric prebid_cache.requests.ok" def initialValue = getCurrentMetricValue(defaultPbsService, CACHE_REQUEST_OK_GLOBAL_METRIC) diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java index ebb573edbe0..6ce3836a695 100644 --- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java +++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java @@ -108,6 +108,7 @@ public void setUp() throws MalformedURLException, JsonProcessingException { target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, null, @@ -233,6 +234,60 @@ public void cacheBidsOpenrtbShouldTolerateReadingHttpResponseFails() throws Json // then verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); + verify(httpClient).post(eq("http://cache-service/cache"), any(), any(), anyLong()); + + final CacheServiceResult result = future.result(); + assertThat(result.getCacheBids()).isEmpty(); + assertThat(result.getError()).isInstanceOf(RuntimeException.class).hasMessage("Response exception"); + + final CacheHttpRequest request = givenCacheHttpRequest(bidinfo.getBid()); + assertThat(result.getHttpCall()) + .isEqualTo(DebugHttpCall.builder() + .requestHeaders(givenDebugHeaders()) + .endpoint("http://cache-service/cache") + .requestBody(request.getBody()) + .requestUri(request.getUri()) + .responseTimeMillis(0) + .build()); + } + + @Test + public void cacheBidsOpenrtbShouldTryCallingInternalEndpointAndTolerateReadingHttpResponseFails() + throws JsonProcessingException, MalformedURLException { + + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + new URL("http://cache-service-internal/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + null, + false, + false, + null, + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + + givenHttpClientProducesException(new RuntimeException("Response exception")); + final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1")); + + // when + final Future future = target.cacheBidsOpenrtb( + singletonList(bidinfo), + givenAuctionContext(), + CacheContext.builder() + .shouldCacheBids(true) + .build(), + eventsContext); + + // then + verify(metrics).updateCacheRequestFailedTime(eq("accountId"), anyLong()); + verify(httpClient).post(eq("http://cache-service-internal/cache"), any(), any(), anyLong()); final CacheServiceResult result = future.result(); assertThat(result.getCacheBids()).isEmpty(); @@ -384,6 +439,7 @@ public void cacheBidsOpenrtbShouldUseApiKeyWhenProvided() throws MalformedURLExc target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -783,6 +839,8 @@ public void cachePutObjectsShould() throws IOException { timeout); // then + verify(httpClient).post(eq("http://cache-service/cache"), any(), any(), anyLong()); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); verify(metrics).updateCacheCreativeSize(eq("account"), eq(4), eq(MetricName.xml)); verify(metrics).updateCacheCreativeSize(eq("account"), eq(11), eq(MetricName.unknown)); @@ -816,12 +874,65 @@ public void cachePutObjectsShould() throws IOException { .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject); } + @Test + public void cachePutObjectsShouldCallInternalCacheEndpointWhenProvided() throws IOException { + // given + target = new CoreCacheService( + httpClient, + new URL("http://cache-service/cache"), + new URL("http://cache-service-internal/cache"), + "http://cache-service-host/cache?uuid=", + 100L, + null, + false, + false, + null, + vastModifier, + eventsService, + metrics, + clock, + idGenerator, + jacksonMapper); + + final BidPutObject firstBidPutObject = BidPutObject.builder() + .type("json") + .bidid("bidId1") + .bidder("bidder1") + .timestamp(1L) + .value(new TextNode("vast")) + .ttlseconds(1) + .build(); + + given(vastModifier.modifyVastXml(any(), any(), any(), any(), anyString())) + .willReturn(new TextNode("modifiedVast")); + + // when + target.cachePutObjects(asList(firstBidPutObject), true, singleton("bidder1"), "account", "pbjs", timeout); + + // then + verify(httpClient).post(eq("http://cache-service-internal/cache"), any(), any(), anyLong()); + verify(metrics).updateCacheCreativeSize(eq("account"), eq(12), eq(MetricName.json)); + verify(metrics).updateCacheCreativeTtl(eq("account"), eq(1), eq(MetricName.json)); + + verify(vastModifier).modifyVastXml(true, singleton("bidder1"), firstBidPutObject, "account", "pbjs"); + + final BidPutObject modifiedFirstBidPutObject = firstBidPutObject.toBuilder() + .bidid(null) + .bidder(null) + .timestamp(null) + .value(new TextNode("modifiedVast")) + .build(); + + assertThat(captureBidCacheRequest().getPuts()).containsExactly(modifiedFirstBidPutObject); + } + @Test public void cachePutObjectsShouldUseApiKeyWhenProvided() throws MalformedURLException { // given target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -863,6 +974,7 @@ public void cacheBidsOpenrtbShouldPrependTraceInfoWhenEnabled() throws IOExcepti target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -932,6 +1044,7 @@ public void cacheBidsOpenrtbShouldPrependTraceInfoWithDatacenterWhenEnabled() th target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -1001,6 +1114,7 @@ public void cacheBidsOpenrtbShouldNotPrependTraceInfoToLowEntoryCacheIds() throw target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -1051,6 +1165,7 @@ public void cachePutObjectsShouldPrependTraceInfoWhenEnabled() throws IOExceptio target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -1098,6 +1213,7 @@ public void cachePutObjectsShouldPrependTraceInfoWithDatacenterWhenEnabled() thr target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", @@ -1145,6 +1261,7 @@ public void cachePutObjectsShouldNotPrependTraceInfoToPassedInKey() throws IOExc target = new CoreCacheService( httpClient, new URL("http://cache-service/cache"), + null, "http://cache-service-host/cache?uuid=", 100L, "ApiKey", From d2129ab3488da6a1266b329ccb8c1d7a49ac1b97 Mon Sep 17 00:00:00 2001 From: Ivan Krdzavac Date: Tue, 10 Jun 2025 14:44:27 +0200 Subject: [PATCH 24/51] Ogury: Enable in app traffic (#3975) --- .../server/bidder/ogury/OguryBidder.java | 19 +++++--- src/main/resources/bidder-config/ogury.yaml | 2 + .../server/bidder/ogury/OguryBidderTest.java | 46 +++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java index 91906d7643c..3626f88b93b 100644 --- a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -70,10 +70,17 @@ public Result>> makeHttpRequests(BidRequest bidRequ } } - if (!isValidRequestKeys(bidRequest, impsWithOguryParams)) { - errors.add(BidderError.badInput( - "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); - return Result.withErrors(errors); + if (CollectionUtils.isEmpty(impsWithOguryParams)) { + if (bidRequest.getApp() != null) { + errors.add(BidderError.badInput("Invalid request. assetKey/adUnitId required")); + return Result.withErrors(errors); + } + // for "site" request we can serve ads with just publisher.id + if (!hasPublisherId(bidRequest)) { + errors.add(BidderError.badInput( + "Invalid request. assetKey/adUnitId or request.site.publisher.id required")); + return Result.withErrors(errors); + } } final BidRequest modifiedBidRequest = bidRequest.toBuilder() @@ -137,8 +144,8 @@ private boolean hasOguryParams(Imp imp) { && impExtBidderHoist.has(PREBID_FIELD_ADUNIT_ID); } - private boolean isValidRequestKeys(BidRequest request, List impsWithOguryParams) { - return !CollectionUtils.isEmpty(impsWithOguryParams) || Optional.ofNullable(request.getSite()) + private boolean hasPublisherId(BidRequest request) { + return Optional.ofNullable(request.getSite()) .map(Site::getPublisher) .map(Publisher::getId) .isPresent(); diff --git a/src/main/resources/bidder-config/ogury.yaml b/src/main/resources/bidder-config/ogury.yaml index 32ccccac07e..90fe12ccd76 100644 --- a/src/main/resources/bidder-config/ogury.yaml +++ b/src/main/resources/bidder-config/ogury.yaml @@ -8,6 +8,8 @@ adapters: maintainer-email: deliveryservices@ogury.co site-media-types: - banner + app-media-types: + - banner vendor-id: 31 usersync: cookie-family-name: ogury diff --git a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java index 8222df6ab01..d5426ab415d 100644 --- a/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/ogury/OguryBidderTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; @@ -191,6 +192,47 @@ public void makeHttpRequestsShouldNotSendImpsWhenHasNotPublisherIdAndImpsWithOgu BidderError.badInput("Invalid request. assetKey/adUnitId or request.site.publisher.id required")); } + @Test + public void makeHttpRequestsAppShouldSendOnlyImpsWithOguryParamsIfPresent() { + // given + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.app(givenApp()), + givenImp(imp -> imp.id("without_ogury_keys").ext(givenEmptyImpExt())), + givenImp(imp -> imp.id("with_ogury_keys").ext(givenImpExtWithOguryKeys()))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getId) + .containsExactly("with_ogury_keys"); + + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsAppShouldNotSendImpsWhenImpsWithOguryIsEmpty() { + // given + final ObjectNode emptyImpExt = givenEmptyImpExt(); + + final BidRequest bidrequest = givenBidRequest( + bidRequest -> bidRequest.app(givenApp()), + givenImp(imp -> imp.id("id1").ext(emptyImpExt)), + givenImp(imp -> imp.id("id2").ext(emptyImpExt))); + + // when + final Result>> result = target.makeHttpRequests(bidrequest); + + // then + assertThat(result.getValue()).isEmpty(); + + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Invalid request. assetKey/adUnitId required")); + } + @Test public void makeHttpRequestsShouldCopyImpIdToTagId() { // given @@ -564,6 +606,10 @@ private Site givenSite() { .build(); } + private App givenApp() { + return App.builder().bundle("app_bundle").build(); + } + private ObjectNode givenEmptyImpExt() { final ObjectNode ext = mapper.createObjectNode(); ext.putIfAbsent("bidder", mapper.createObjectNode()); From c8c0c13ce51505b6177e089d68111aec36e934d1 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:55:48 +0200 Subject: [PATCH 25/51] Bid Ranking (#3977) --- .../server/auction/BidResponseCreator.java | 164 +++-- .../prebid/server/auction/model/BidInfo.java | 2 + .../server/auction/model/TargetingInfo.java | 2 - .../openrtb/ext/response/ExtBidPrebid.java | 2 + .../settings/model/AccountAuctionConfig.java | 2 + .../model/AccountBidRankingConfig.java | 9 + .../model/config/AccountAuctionConfig.groovy | 1 + .../model/config/AccountRankingConfig.groovy | 9 + .../model/response/auction/Bid.groovy | 2 +- .../model/response/auction/Prebid.groovy | 1 + .../functional/tests/TargetingSpec.groovy | 571 +++++++++++++++++- .../PriceFloorsAdjustmentSpec.groovy | 2 +- .../auction/BidResponseCreatorTest.java | 251 +++++++- 13 files changed, 889 insertions(+), 129 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index ee28ddbe9d7..dd8f79c1920 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -23,6 +23,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; @@ -101,6 +102,7 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountAuctionEventConfig; +import org.prebid.server.settings.model.AccountBidRankingConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountTargetingConfig; import org.prebid.server.settings.model.VideoStoredDataResult; @@ -114,9 +116,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -124,6 +126,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; public class BidResponseCreator { @@ -644,7 +647,9 @@ private Future cacheBidsAndCreateResponse(List final ExtRequestTargeting targeting = targeting(bidRequest); final List bidderResponseInfos = toBidderResponseWithTargetingBidInfos( - bidderResponses, bidderToMultiBids, preferDeals(targeting)); + bidderResponses, + bidderToMultiBids, + preferDeals(targeting)); final Set bidInfos = bidderResponseInfos.stream() .map(BidderResponseInfo::getSeatBid) @@ -687,76 +692,81 @@ private List toBidderResponseWithTargetingBidInfos( Map bidderToMultiBids, boolean preferDeals) { - final Map> bidderResponseToReducedBidInfos = bidderResponses.stream() - .collect(Collectors.toMap( - Function.identity(), - bidderResponse -> toSortedMultiBidInfo(bidderResponse, bidderToMultiBids, preferDeals))); - - final Map>> impIdToBidderToBidInfos = bidderResponseToReducedBidInfos.values() - .stream() - .flatMap(Collection::stream) - .collect(Collectors.groupingBy( - bidInfo -> bidInfo.getCorrespondingImp().getId(), - Collectors.groupingBy(BidInfo::getBidder))); - - // Best bids from bidders for imp - final Set winningBids = new HashSet<>(); - // All bids from bidder for imp - final Set winningBidsByBidder = new HashSet<>(); - - for (final Map> bidderToBidInfos : impIdToBidderToBidInfos.values()) { + final Comparator comparator = winningBidComparatorFactory.create(preferDeals).reversed(); - bidderToBidInfos.values().forEach(winningBidsByBidder::addAll); - - bidderToBidInfos.values().stream() - .flatMap(Collection::stream) - .max(winningBidComparatorFactory.create(preferDeals)) - .ifPresent(winningBids::add); - } + final List> bidInfosPerBidder = bidderResponses.stream() + .map(bidderResponse -> limitMultiBid(bidderResponse, bidderToMultiBids, comparator)) + .toList(); + final List> rankedBidInfos = applyRanking(bidInfosPerBidder, comparator); - return bidderResponseToReducedBidInfos.entrySet().stream() - .map(responseToBidInfos -> injectBidInfoWithTargeting( - responseToBidInfos.getKey(), - responseToBidInfos.getValue(), - bidderToMultiBids, - winningBids, - winningBidsByBidder)) + return IntStream.range(0, bidderResponses.size()) + .mapToObj(i -> enrichBidInfoWithTargeting( + bidderResponses.get(i), + rankedBidInfos.get(i), + bidderToMultiBids)) .toList(); } - private List toSortedMultiBidInfo(BidderResponseInfo bidderResponse, + private static List limitMultiBid(BidderResponseInfo bidderResponse, Map bidderToMultiBids, - boolean preferDeals) { + Comparator comparator) { + + final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); + final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; final List bidInfos = bidderResponse.getSeatBid().getBidsInfos(); final Map> impIdToBidInfos = bidInfos.stream() .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); - final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); - final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; - return impIdToBidInfos.values().stream() - .map(infos -> sortReducedBidInfo(infos, bidLimit, preferDeals)) - .flatMap(Collection::stream) + .flatMap(infos -> infos.stream() + .sorted(comparator) + .limit(bidLimit)) .toList(); } - private List sortReducedBidInfo(List bidInfos, int limit, boolean preferDeals) { - return bidInfos.stream() - .sorted(winningBidComparatorFactory.create(preferDeals).reversed()) - .limit(limit) - .toList(); + private static List> applyRanking(List> bidInfosPerBidder, + Comparator comparator) { + + final Map>> impIdToBidderBidInfo = new HashMap<>(); + for (int bidderIndex = 0; bidderIndex < bidInfosPerBidder.size(); bidderIndex++) { + final List bidInfos = bidInfosPerBidder.get(bidderIndex); + + for (BidInfo bidInfo : bidInfos) { + impIdToBidderBidInfo + .computeIfAbsent(bidInfo.getCorrespondingImp().getId(), ignore -> new ArrayList<>()) + .add(Pair.of(bidderIndex, bidInfo)); + } + } + + for (List> bidderToBidInfo : impIdToBidderBidInfo.values()) { + bidderToBidInfo.sort(Comparator.comparing(Pair::getRight, comparator)); + } + + final List> rankedBidInfosPerBidder = new ArrayList<>(); + for (int i = 0; i < bidInfosPerBidder.size(); i++) { + rankedBidInfosPerBidder.add(new ArrayList<>()); + } + + for (List> sortedBidderToBidInfo : impIdToBidderBidInfo.values()) { + for (int rank = 0; rank < sortedBidderToBidInfo.size(); rank++) { + final Pair bidderToBidInfo = sortedBidderToBidInfo.get(rank); + final BidInfo bidInfo = bidderToBidInfo.getRight(); + + rankedBidInfosPerBidder.get(bidderToBidInfo.getLeft()) + .add(bidInfo.toBuilder().rank(rank + 1).build()); + } + } + + return rankedBidInfosPerBidder; } - private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, + private static BidderResponseInfo enrichBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, List bidderBidInfos, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + Map bidderToMultiBids) { final String bidder = bidderResponseInfo.getBidder(); - final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids, - winningBids, winningBidsByBidder); + final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids); final BidderSeatBidInfo seatBid = bidderResponseInfo.getSeatBid(); final BidderSeatBidInfo modifiedSeatBid = seatBid.with(bidInfosWithTargeting); @@ -765,24 +775,20 @@ private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo private static List toBidInfoWithTargeting(List bidderBidInfos, String bidder, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + Map bidderToMultiBids) { final Map> impIdToBidInfos = bidderBidInfos.stream() .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); return impIdToBidInfos.values().stream() - .map(bidInfos -> injectTargeting(bidInfos, bidder, bidderToMultiBids, winningBids, winningBidsByBidder)) + .map(bidInfos -> enrichWithTargeting(bidInfos, bidder, bidderToMultiBids)) .flatMap(Collection::stream) .toList(); } - private static List injectTargeting(List bidderImpIdBidInfos, - String bidder, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + private static List enrichWithTargeting(List bidderImpIdBidInfos, + String bidder, + Map bidderToMultiBids) { final List result = new ArrayList<>(); @@ -797,8 +803,7 @@ private static List injectTargeting(List bidderImpIdBidInfos, final TargetingInfo targetingInfo = TargetingInfo.builder() .isTargetingEnabled(targetingBidderCode != null) - .isBidderWinningBid(winningBidsByBidder.contains(bidInfo)) - .isWinningBid(winningBids.contains(bidInfo)) + .isWinningBid(bidInfo.getRank() == 1) .isAddTargetBidderCode(targetingBidderCode != null && multiBidSize > 1) .bidderCode(targetingBidderCode) .seat(targetingCode(bidInfo.getSeat(), bidderCodePrefix, i)) @@ -819,10 +824,6 @@ private static String targetingCode(String base, String prefix, int i) { return prefix != null ? prefix + (i + 1) : null; } - /** - * Returns {@link ExtBidResponse} object, populated with response time, errors and debug info (if requested) - * from all bidders. - */ private ExtBidResponse toExtBidResponse(List bidderResponseInfos, AuctionContext auctionContext, CacheServiceResult cacheResult, @@ -1544,6 +1545,7 @@ private Bid toBid(BidInfo bidInfo, BidRequest bidRequest, Account account, Map> bidWarnings) { + final TargetingInfo targetingInfo = bidInfo.getTargetingInfo(); final BidType bidType = bidInfo.getBidType(); final Bid bid = bidInfo.getBid(); @@ -1575,6 +1577,8 @@ private Bid toBid(BidInfo bidInfo, final ObjectNode originalBidExt = bid.getExt(); final Boolean dealsTierSatisfied = bidInfo.getSatisfiedPriority(); + final boolean bidRankingEnabled = isBidRankingEnabled(account); + final ExtBidPrebid updatedExtBidPrebid = getExtPrebid(originalBidExt, ExtBidPrebid.class) .map(ExtBidPrebid::toBuilder) @@ -1584,6 +1588,7 @@ private Bid toBid(BidInfo bidInfo, .dealTierSatisfied(dealsTierSatisfied) .cache(cache) .passThrough(extractPassThrough(bidInfo.getCorrespondingImp())) + .rank(bidRankingEnabled ? bidInfo.getRank() : null) .build(); final ObjectNode updatedBidExt = @@ -1601,7 +1606,6 @@ private Bid toBid(BidInfo bidInfo, private boolean shouldIncludeTargetingInResponse(ExtRequestTargeting targeting, TargetingInfo targetingInfo) { return targeting != null && targetingInfo.isTargetingEnabled() - && targetingInfo.isBidderWinningBid() && (Objects.equals(targeting.getIncludebidderkeys(), true) || Objects.equals(targeting.getIncludewinners(), true) || Objects.equals(targeting.getIncludeformat(), true)); @@ -1614,6 +1618,13 @@ private JsonNode extractPassThrough(Imp imp) { .orElse(null); } + private static boolean isBidRankingEnabled(Account account) { + return Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getRanking) + .map(AccountBidRankingConfig::getEnabled) + .orElse(false); + } + private String createNativeMarkup(String bidAdm, Imp correspondingImp) { final Response nativeMarkup; try { @@ -1740,9 +1751,6 @@ private static boolean eventsAllowedByRequest(AuctionContext auctionContext) { return prebid != null && prebid.getEvents() != null; } - /** - * Extracts auction timestamp from {@link ExtRequest} or get it from {@link Clock} if it is null. - */ private long auctionTimestamp(AuctionContext auctionContext) { final ExtRequest ext = auctionContext.getBidRequest().getExt(); final ExtRequestPrebid prebid = ext != null ? ext.getPrebid() : null; @@ -1872,9 +1880,6 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe resolveKeyPrefix); } - /** - * Returns max targeting keyword length. - */ private int resolveTruncateAttrChars(ExtRequestTargeting targeting, Account account) { final AccountAuctionConfig accountAuctionConfig = account.getAuction(); final Integer accountTruncateTargetAttr = @@ -1929,11 +1934,6 @@ private static boolean isCachedDebugEnabled(CachedDebugLog cachedDebugLog) { return cachedDebugLog != null && cachedDebugLog.isEnabled(); } - /** - * Parse {@link JsonNode} to {@link List} of {@link ExtPriceGranularity}. - *

- * Throws {@link PreBidException} in case of errors during decoding price granularity. - */ private ExtPriceGranularity parsePriceGranularity(JsonNode priceGranularity) { try { return mapper.mapper().treeToValue(priceGranularity, ExtPriceGranularity.class); @@ -1969,9 +1969,6 @@ private static BidResponse populateSeatNonBid(AuctionContext auctionContext, Bid return bidResponse.toBuilder().ext(updatedExtBidResponse).build(); } - /** - * Creates {@link CacheAsset} for the given cache ID. - */ private CacheAsset toCacheAsset(String cacheId) { return CacheAsset.of(cacheAssetUrlTemplate.concat(cacheId), cacheId); } @@ -1983,9 +1980,6 @@ private static Set nullIfEmpty(Set set) { return Collections.unmodifiableSet(set); } - /** - * Creates {@link ExtBidPrebidVideo} from bid extension. - */ private Optional getExtBidPrebidVideo(ObjectNode bidExt) { return getExtPrebid(bidExt, ExtBidPrebid.class) .map(ExtBidPrebid::getVideo); diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java index f2e3fc7b438..224b6344f34 100644 --- a/src/main/java/org/prebid/server/auction/model/BidInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -37,6 +37,8 @@ public class BidInfo { Integer vastTtl; + Integer rank; + public String getBidId() { final ObjectNode extNode = bid != null ? bid.getExt() : null; final JsonNode bidIdNode = extNode != null ? extNode.path("prebid").path("bidid") : null; diff --git a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java index dbd3d7fd2b3..60f453a942f 100644 --- a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java +++ b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java @@ -15,7 +15,5 @@ public class TargetingInfo { boolean isWinningBid; - boolean isBidderWinningBid; - boolean isAddTargetBidderCode; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java index d116447caed..afc46770799 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java @@ -40,4 +40,6 @@ public class ExtBidPrebid { @JsonProperty("passthrough") JsonNode passThrough; + + Integer rank; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index 82bf01afb75..e41f005df54 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -58,4 +58,6 @@ public class AccountAuctionConfig { PaaFormat paaFormat; AccountCacheConfig cache; + + AccountBidRankingConfig ranking; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java b/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java new file mode 100644 index 00000000000..361ff4c9781 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountBidRankingConfig { + + Boolean enabled; +} 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 d0b3ee586d2..bf49ce7c874 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 @@ -24,6 +24,7 @@ class AccountAuctionConfig { AccountBidValidationConfig bidValidations AccountEventsConfig events AccountCacheConfig cache + AccountRankingConfig ranking AccountPriceFloorsConfig priceFloors Targeting targeting PaaFormat paaformat diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy new file mode 100644 index 00000000000..64103717127 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountRankingConfig { + + Boolean enabled +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index 353f5516935..18bc588d1d3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -72,7 +72,7 @@ class Bid implements ObjectMapperWrapper { } } - static List getDefaultMultyTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { + static List getDefaultMultiTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { List bids = [] if (imp.banner) bids << createBid(imp, BidMediaType.BANNER) { adm = null } if (imp.video) bids << createBid(imp, BidMediaType.VIDEO) diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy index 35fa5b6a540..d8accbf82ac 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy @@ -15,4 +15,5 @@ class Prebid { Meta meta Map passThrough Video storedRequestAttributes + Integer rank } 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 e24d22b4b8f..081c578ecfa 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,20 +4,34 @@ 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.AccountRankingConfig import org.prebid.server.functional.model.config.PriceGranularityType import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.AdServerTargeting import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.MultiBid +import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidCache +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest import org.prebid.server.functional.model.request.auction.PriceGranularity import org.prebid.server.functional.model.request.auction.Range +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting +import org.prebid.server.functional.model.request.auction.Video import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidMediaType import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.model.response.auction.Prebid +import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils @@ -28,8 +42,11 @@ 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.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD 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.model.response.auction.MediaType.VIDEO import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class TargetingSpec extends BaseSpec { @@ -40,7 +57,10 @@ class TargetingSpec extends BaseSpec { private static final String DEFAULT_TARGETING_PREFIX = "hb" private static final Integer TARGETING_PREFIX_LENGTH = 11 private static final Integer MAX_TRUNCATE_ATTR_CHARS = 255 + private static final Integer MAX_BIDS_RANKING = 3 private static final String HB_ENV_AMP = "amp" + private static final Integer MAIN_RANK = 1 + private static final Integer SUBORDINATE_RANK = 2 def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -1166,7 +1186,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) } def "PBS amp should prioritize price granularity from original request over account config"() { @@ -1196,7 +1216,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) } def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() { @@ -1217,7 +1237,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + 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"() { @@ -1238,7 +1258,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + 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"() { @@ -1301,7 +1321,6 @@ class TargetingSpec extends BaseSpec { 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) @@ -1341,13 +1360,549 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) } - def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def "PBS shouldn't add bid ranked for request when account config for auction.ranking disabled or default"() { + given: "Bid request with enabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + 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) + + and: "Bid response with 3 bids where deal bid has higher price" + def imp = bidRequest.imp.first + def bids = [Bid.getDefaultBid(imp), Bid.getDefaultBid(imp), Bid.getDefaultBid(imp)] + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = bids + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS bids in response shouldn't contain ranks" + assert response?.seatbid?.bid?.ext?.prebid?.rank?.flatten() == [null] * MAX_BIDS_RANKING + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(ranking: new AccountRankingConfig()), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: null)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: false)) + ] + } + + def "PBS should add bid ranked and rank by deals for default request when auction.ranking and preferDeals are enabled"() { + given: "Bid request with enabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = true + } + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidWithDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidWithDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank single bid" + verifyAll(response.seatbid.first.bid) { + it.id == [bidWithDeal.id] + it.price == [bidWithDeal.price] + it.ext.prebid.rank == [MAIN_RANK] + } + } + + def "PBS should add bid ranked and rank by price for default request when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidWithDealId = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidWithDealId] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank single bid" + verifyAll(response.seatbid.first.bid) { + it.id == [bidBiggerPrice.id] + it.price == [bidBiggerPrice.price] + it.ext.prebid.rank == [MAIN_RANK] + } + } + + def "PBS should add bid ranked and rank by price for request with multiBid when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should add bid ranked and rank by price for multiple media types request when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.video = Video.getDefaultVideo() + it.imp.first.nativeObj = Native.getDefaultNative() + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultMultiTypesBids(bidRequest.imp.first).first.tap { + it.price = bidPrice + 1 + } + def bidBDeal = Bid.getDefaultMultiTypesBids(bidRequest.imp.first).last.tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + assert !response.ext.warnings + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should properly rank bids when request with multibid contains some invalid bid"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def higherPriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 2 + } + + def middlePriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 1 + adm = null + } + + def lowerPriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [lowerPriceBid, middlePriceBid, higherPriceBid] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == higherPriceBid.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == lowerPriceBid.id).ext.prebid.rank == 2 + + and: "PBS should contain error for invalid bid" + response.ext.errors[ErrorType.GENERIC]?.message == + ["BidId `${middlePriceBid.id}` validation messages: Error: Bid \"${middlePriceBid.id}\" with video type missing adm and nurl"] + } + + def "PBS should assign bid ranks across all seatbids combined when the request contains imps with multiple bidders"() { + given: "PBS config with openX bidder" + def pbsConfig = ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with multiple bidders" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = true + } + it.ext.prebid.multibid = [new MultiBid(bidder: WILDCARD, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def genericBid = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def openxBid = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [genericBid], seat: GENERIC), + new SeatBid(bid: [openxBid], seat: OPENX)] + } + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS should rank OpenX bid higher than Generic bid" + assert response.seatbid.findAll { it.seat == OPENX }.bid.ext.prebid.rank.flatten() == [MAIN_RANK] + assert response.seatbid.findAll { it.seat == GENERIC }.bid.ext.prebid.rank.flatten() == [SUBORDINATE_RANK] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should assign bid ranks for each imp separately when request has multiple imps and multiBid is configured"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.nativeObj = Native.getDefaultNative() + imp.add(Imp.getDefaultImpression(VIDEO)) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = requestPreferDeals + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def bidLowerPrice = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + mediaType = BidMediaType.NATIVE + } + def bidHigherPrice = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 1 + } + def bidWithDeal = Bid.getDefaultBid(bidRequest.imp.last).tap { + dealid = PBSUtils.randomNumber + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidLowerPrice, bidHigherPrice, bidWithDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bids for first imp" + def bids = response.seatbid.first.bid + def firstImpBidders = bids.findAll { it.impid == bidRequest.imp.id.first() } + assert firstImpBidders.find { it.id == bidHigherPrice.id }.ext.prebid.rank == 1 + assert firstImpBidders.find { it.id == bidLowerPrice.id }.ext.prebid.rank == 2 + + and: "should separately rank bids for second imp" + def secondImpBidders = bids.findAll { it.impid == bidRequest.imp.id.last() } + assert secondImpBidders*.ext.prebid.rank == [MAIN_RANK] + + where: + requestPreferDeals << [null, false, true] + } + + def "PBS should ignore bid ranked from original response when auction.ranking enabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + it.ext = new BidExt(prebid: new Prebid(rank: PBSUtils.randomNumber)) + } + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + it.ext = new BidExt(prebid: new Prebid(rank: PBSUtils.randomNumber)) + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should add bid ranked and rank by price for request with stored imp when auction.ranking enabled"() { + given: "Bid request with disabled preferDeals" + def storedRequestId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Save storedImp into DB" + def impression = Imp.getDefaultImpression(MediaType.BANNER).tap { + id = storedRequestId + video = Video.getDefaultVideo() + } + def storedImp = StoredImp.getStoredImp(bidRequest.accountId, impression) + storedImpDao.save(storedImp) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultMultiTypesBids(impression).first.tap { + it.price = bidPrice + 1 + impid = bidRequest.imp.id.first + } + def bidBDeal = Bid.getDefaultMultiTypesBids(impression).last.tap { + impid = bidRequest.imp.id.first + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS shouldn't rank bids for request with stored imp when auction.ranking default"() { + given: "Bid request with enabled preferDeals" + def storedRequestId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: new AccountAuctionConfig()) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Save storedImp into DB" + def impression = Imp.getDefaultImpression(MediaType.BANNER).tap { + id = storedRequestId + video = Video.getDefaultVideo() + nativeObj = Native.getDefaultNative() + } + def storedImp = StoredImp.getStoredImp(bidRequest.accountId, impression) + storedImpDao.save(storedImp) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = Bid.getDefaultMultiTypesBids(impression) { impid = bidRequest.imp.id.first } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS bids in response shouldn't contain ranks" + assert response?.seatbid?.bid?.ext?.prebid?.rank?.flatten() == [null] * MAX_BIDS_RANKING + } + + def "PBS should copy bid ranked from stored response when auction.ranking #auction"() { + given: "Bid request with enabled preferDeals" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: auction) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Stored response in DB" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPriceRanking = PBSUtils.randomNumber + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + it.ext = new BidExt(prebid: new Prebid(rank: bidBiggerPriceRanking)) + } + def bidBDealRanking = PBSUtils.randomNumber + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + it.ext = new BidExt(prebid: new Prebid(rank: bidBDealRanking)) + } + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: new SeatBid(bid: [bidBiggerPrice, bidBDeal], seat: GENERIC)) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should copy bid ranked from stored response" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == bidBiggerPriceRanking + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == bidBDealRanking + + where: + auction << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(ranking: new AccountRankingConfig()), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: null)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: false)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: true)) + ] + } + + Account 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) + new Account(uuid: accountId, config: accountConfig) + } + + Account getAccountConfigWithAuctionRanking(String accountId, Boolean auctionRankingEnablement = true) { + def accountAuctionConfig = new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: auctionRankingEnablement)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) } private static PrebidServerService getEnabledWinBidsPbsService() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy index a1fa69e487e..c04e4d263d7 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy @@ -636,7 +636,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { def originalPrice = PBSUtils.getRandomDecimal() def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = currency - seatbid.first.bid = Bid.getDefaultMultyTypesBids(bidRequest.imp.first) { + seatbid.first.bid = Bid.getDefaultMultiTypesBids(bidRequest.imp.first) { price = originalPrice ext = new BidExt() } diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index f62c6a44651..83deee74ce8 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -88,7 +88,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.TraceLevel; -import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.CacheAsset; import org.prebid.server.proto.openrtb.ext.response.Events; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; @@ -118,6 +117,7 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountAuctionEventConfig; +import org.prebid.server.settings.model.AccountBidRankingConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.VideoStoredDataResult; import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; @@ -161,7 +161,6 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic; import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; @@ -304,8 +303,22 @@ public void shouldPassBidWithGeneratedIdAndPreserveExtFieldsWhenIdGeneratorTypeU .put("origbidcur", "test") .set("prebid", mapper.valueToTree(extBidPrebid))) .build(); - final BidInfo bidInfo = toBidInfo(expectedBid, imp, bidder, "seat", banner, true); - verify(coreCacheService).cacheBidsOpenrtb(eq(singletonList(bidInfo)), any(), any(), any()); + final BidInfo expectedBidInfo = BidInfo.builder() + .bid(expectedBid) + .correspondingImp(imp) + .bidder("bidder1") + .seat("seat") + .bidType(banner) + .targetingInfo(TargetingInfo.builder() + .bidderCode("bidder1") + .seat("seat") + .isTargetingEnabled(true) + .isWinningBid(true) + .isAddTargetBidderCode(false) + .build()) + .rank(1) + .build(); + verify(coreCacheService).cacheBidsOpenrtb(eq(singletonList(expectedBidInfo)), any(), any(), any()); } @Test @@ -955,7 +968,22 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenIdGeneratorIsUUIDAndEvent .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid))) .build(); - final BidInfo expectedBidInfo = toBidInfo(expectedBid, imp, bidder, "seat", banner, true); + final BidInfo expectedBidInfo = BidInfo.builder() + .bid(expectedBid) + .correspondingImp(imp) + .bidder("bidder1") + .seat("seat") + .bidType(banner) + .targetingInfo(TargetingInfo.builder() + .bidderCode("bidder1") + .seat("seat") + .isTargetingEnabled(true) + .isWinningBid(true) + .isAddTargetBidderCode(false) + .build()) + .rank(1) + .build(); + verify(coreCacheService).cacheBidsOpenrtb(eq(singletonList(expectedBidInfo)), any(), any(), any()); verify(eventsService).createEvent(eq(generatedBidId), anyString(), anyString(), anyBoolean(), any()); @@ -985,7 +1013,7 @@ public void shouldSetExpectedResponseSeatBidAndBidFields() { .ext(bidExt) .build(); - final String bidder = "bidder1"; + final String bidder = "bidder"; final List bidderResponses = singletonList(BidderResponse.of( bidder, givenSeatBid( @@ -1042,6 +1070,189 @@ public void shouldSetExpectedResponseSeatBidAndBidFields() { .build()); } + @Test + public void shouldSetExpectedBidsWithRanksWhenBidRankingEnabled() { + // given + final ObjectNode bidExt = mapper.createObjectNode() + .put("origbidcpm", BigDecimal.ONE) + .put("origbidcur", "USD"); + + final Bid bid1 = Bid.builder() + .id("bidId1") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .impid("impId1") + .ext(bidExt) + .build(); + + final Bid bid2 = Bid.builder() + .id("bidId2") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .impid("impId2") + .ext(bidExt) + .build(); + + final String bidder = "bidder"; + final List bidderResponses = singletonList(BidderResponse.of( + bidder, + givenSeatBid( + BidderBid.of(bid1, banner, "seat1", "USD"), + BidderBid.of(bid2, banner, "seat2", "USD")), + 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(givenImp("impId1"), givenImp("impId2")), + contextBuilder -> contextBuilder + .auctionParticipations(toAuctionParticipant(bidderResponses)) + .account(Account.builder().auction(AccountAuctionConfig.builder() + .ranking(AccountBidRankingConfig.of(true)).build()) + .build())); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); + givenCacheServiceResult(emptyList()); + + // when + final BidResponse bidResponse = target.create(auctionContext, cacheInfo, MULTI_BIDS).result(); + + // then + final ObjectNode expectedBidExtToCache = mapper.createObjectNode(); + expectedBidExtToCache.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build())); + expectedBidExtToCache.put("origbidcpm", BigDecimal.ONE); + expectedBidExtToCache.put("origbidcur", "USD"); + + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + verify(coreCacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any()); + + assertThat(bidsArgumentCaptor.getValue()) + .extracting(bidInfo -> bidInfo.getBid().getExt()) + .containsOnly(expectedBidExtToCache); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().rank(1).type(banner).build())); + expectedBidExt.put("origbidcpm", BigDecimal.ONE); + expectedBidExt.put("origbidcur", "USD"); + + assertThat(bidResponse.getSeatbid()) + .containsOnly( + SeatBid.builder() + .seat("seat1") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId1") + .impid("impId1") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .ext(expectedBidExt) + .build())) + .build(), + SeatBid.builder() + .seat("seat2") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId2") + .impid("impId2") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .ext(expectedBidExt) + .build())) + .build()); + } + + @Test + public void shouldSetExpectedBidsWithRanksWhenBidHasSameImpIdAndBidRankingEnabled() { + // given + final ObjectNode bidExt = mapper.createObjectNode() + .put("origbidcpm", BigDecimal.ONE) + .put("origbidcur", "USD"); + + final Bid bid1 = Bid.builder() + .id("bidId1") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .impid("impId") + .ext(bidExt) + .build(); + + final Bid bid2 = Bid.builder() + .id("bidId2") + .price(BigDecimal.TWO) + .adm(BID_ADM) + .impid("impId") + .ext(bidExt) + .build(); + + final String bidder = "bidder"; + final List bidderResponses = singletonList(BidderResponse.of( + bidder, + givenSeatBid( + BidderBid.of(bid1, banner, "seat1", "USD"), + BidderBid.of(bid2, banner, "seat2", "USD")), + 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(givenImp("impId")), + contextBuilder -> contextBuilder + .auctionParticipations(toAuctionParticipant(bidderResponses)) + .account(Account.builder().auction(AccountAuctionConfig.builder() + .ranking(AccountBidRankingConfig.of(true)).build()) + .build())); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); + givenCacheServiceResult(emptyList()); + + // when + final BidResponse bidResponse = target.create(auctionContext, cacheInfo, MULTI_BIDS).result(); + + // then + final ObjectNode expectedBidExtToCache = mapper.createObjectNode(); + expectedBidExtToCache.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build())); + expectedBidExtToCache.put("origbidcpm", BigDecimal.ONE); + expectedBidExtToCache.put("origbidcur", "USD"); + + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + verify(coreCacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any()); + + assertThat(bidsArgumentCaptor.getValue()) + .extracting(bidInfo -> bidInfo.getBid().getExt()) + .containsOnly(expectedBidExtToCache); + + final ObjectNode expectedBidExtWithRank1 = mapper.createObjectNode(); + expectedBidExtWithRank1.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().rank(1).type(banner).build())); + expectedBidExtWithRank1.put("origbidcpm", BigDecimal.ONE); + expectedBidExtWithRank1.put("origbidcur", "USD"); + + final ObjectNode expectedBidExtWithRank2 = mapper.createObjectNode(); + expectedBidExtWithRank2.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().rank(2).type(banner).build())); + expectedBidExtWithRank2.put("origbidcpm", BigDecimal.ONE); + expectedBidExtWithRank2.put("origbidcur", "USD"); + + assertThat(bidResponse.getSeatbid()) + .containsOnly( + SeatBid.builder() + .seat("seat1") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId1") + .impid("impId") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .ext(expectedBidExtWithRank2) + .build())) + .build(), + SeatBid.builder() + .seat("seat2") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId2") + .impid("impId") + .price(BigDecimal.TWO) + .adm(BID_ADM) + .ext(expectedBidExtWithRank1) + .build())) + .build()); + } + @Test public void shouldUpdateCacheDebugLogWithExtBidResponseWhenEnabledAndBidsReturned() { // given @@ -1647,7 +1858,7 @@ public void shouldPassPreferDealsToWinningComparatorFactoryWhenBidRequestTrue() target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - verify(winningBidComparatorFactory, times(2)).create(eq(true)); + verify(winningBidComparatorFactory).create(eq(true)); } @Test @@ -1667,7 +1878,7 @@ public void shouldPassPreferDealsFalseWhenBidRequestPreferDealsIsNotDefined() { target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - verify(winningBidComparatorFactory, times(2)).create(eq(false)); + verify(winningBidComparatorFactory).create(eq(false)); } @Test @@ -5456,30 +5667,6 @@ private static Map zipBidsWithCacheInfos(List bidInfos, .collect(Collectors.toMap(i -> bidInfos.get(i).getBid(), cacheInfos::get)); } - private static BidInfo toBidInfo(Bid bid, - Imp correspondingImp, - String bidder, - String seat, - BidType bidType, - boolean isWinningBid) { - - return BidInfo.builder() - .bid(bid) - .correspondingImp(correspondingImp) - .bidder(bidder) - .seat(seat) - .bidType(bidType) - .targetingInfo(TargetingInfo.builder() - .bidderCode(bidder) - .seat(seat) - .isTargetingEnabled(true) - .isWinningBid(isWinningBid) - .isBidderWinningBid(true) - .isAddTargetBidderCode(false) - .build()) - .build(); - } - private static Imp givenImp() { return givenImp(IMP_ID); } From c7477c73e01a4d5720eff3c38a822d32a4dfb256 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:58:47 +0300 Subject: [PATCH 26/51] Fix flaky functional test (#4009) --- .../testcontainers/scaffolding/Bidder.groovy | 12 +++++------- .../functional/tests/TargetingSpec.groovy | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy index ff0b9f3b4f7..fd89add53eb 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/Bidder.groovy @@ -19,25 +19,23 @@ import static org.mockserver.model.JsonPathBody.jsonPath class Bidder extends NetworkScaffolding { - private static final String AUCTION_ENDPOINT = "/auction" - - Bidder(MockServerContainer mockServerContainer) { - super(mockServerContainer, AUCTION_ENDPOINT) + Bidder(MockServerContainer mockServerContainer, String endpoint = "/auction") { + super(mockServerContainer, endpoint) } @Override protected HttpRequest getRequest(String bidRequestId) { - request().withPath(AUCTION_ENDPOINT) + request().withPath(endpoint) .withBody(jsonPath("\$[?(@.id == '$bidRequestId')]")) } @Override protected HttpRequest getRequest() { - request().withPath(AUCTION_ENDPOINT) + request().withPath(endpoint) } HttpRequest getRequest(String bidRequestId, String requestMatchPath) { - request().withPath(AUCTION_ENDPOINT) + request().withPath(endpoint) .withBody(jsonPath("\$[?(@.$requestMatchPath == '$bidRequestId')]")) } 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 081c578ecfa..960e05b458e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -34,6 +34,7 @@ import org.prebid.server.functional.model.response.auction.Prebid import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.Bidder import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode @@ -1613,9 +1614,11 @@ class TargetingSpec extends BaseSpec { def "PBS should assign bid ranks across all seatbids combined when the request contains imps with multiple bidders"() { given: "PBS config with openX bidder" + def endpoint = '/openx-auction' def pbsConfig = ["adapters.openx.enabled" : "true", - "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + "adapters.openx.endpoint": "$networkServiceContainer.rootUri$endpoint".toString()] def prebidServerService = pbsServiceFactory.getService(pbsConfig) + def openxBidder = new Bidder(networkServiceContainer, endpoint) and: "Bid request with multiple bidders" def bidRequest = BidRequest.getDefaultBidRequest().tap { @@ -1640,12 +1643,15 @@ class TargetingSpec extends BaseSpec { it.dealid = PBSUtils.randomNumber it.price = bidPrice } - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - it.seatbid = [new SeatBid(bid: [genericBid], seat: GENERIC), - new SeatBid(bid: [openxBid], seat: OPENX)] + def bidResponseGeneric = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [genericBid], seat: GENERIC)] + } + def bidResponseOpenx = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [openxBid], seat: OPENX)] } and: "Set bidder response" - bidder.setResponse(bidRequest.id, bidResponse) + bidder.setResponse(bidRequest.id, bidResponseGeneric) + openxBidder.setResponse(bidRequest.id, bidResponseOpenx) when: "PBS processes auction request" def response = prebidServerService.sendAuctionRequest(bidRequest) @@ -1654,8 +1660,9 @@ class TargetingSpec extends BaseSpec { assert response.seatbid.findAll { it.seat == OPENX }.bid.ext.prebid.rank.flatten() == [MAIN_RANK] assert response.seatbid.findAll { it.seat == GENERIC }.bid.ext.prebid.rank.flatten() == [SUBORDINATE_RANK] - cleanup: "Stop and remove pbs container" + cleanup: "Stop and remove pbs container and bidder response" pbsServiceFactory.removeContainer(pbsConfig) + openxBidder.reset() } def "PBS should assign bid ranks for each imp separately when request has multiple imps and multiBid is configured"() { From 1bbd92f2cc5316653d5f687fb81036b9b121d282 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:56:12 +0300 Subject: [PATCH 27/51] Prebid Server prepare release 3.27.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 2b519bf7492..db6006c414b 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.27.0-SNAPSHOT + 3.27.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 01fab8143a8..6aa88a0fd1b 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index d5a5f3f3203..b90d1df1472 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index 8037f32a55f..3ff6124ff80 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index b951e1e2093..d2060ad0fd4 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index 8c92f5df82a..7c29bbb3871 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 7117b2e7c14..b5e89aed6c7 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 39c22b2bca8..56d10733ac4 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.27.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 200e5f7458d..16576b211aa 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.27.0-SNAPSHOT + 3.27.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 352acbd59f8..cbd35721eb6 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.27.0-SNAPSHOT + 3.27.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.27.0 diff --git a/pom.xml b/pom.xml index fc4794a1772..ac84ca6d391 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.27.0-SNAPSHOT + 3.27.0 extra/pom.xml From 61a88872dcdbe55ccbbdeb862b028ca2dc63c232 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Thu, 12 Jun 2025 17:24:18 +0300 Subject: [PATCH 28/51] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index db6006c414b..7c9e884577f 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.27.0 + 3.28.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 6aa88a0fd1b..26c96b1fd81 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index b90d1df1472..ae160a05610 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index 3ff6124ff80..d6974c7b64a 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index d2060ad0fd4..29dd458e62a 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index 7c29bbb3871..480193461c5 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index b5e89aed6c7..045b0c53869 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 56d10733ac4..027a7a5b6b0 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0 + 3.28.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 16576b211aa..84187c374ce 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.27.0 + 3.28.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index cbd35721eb6..65bc83f7e6b 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.27.0 + 3.28.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.27.0 + HEAD diff --git a/pom.xml b/pom.xml index ac84ca6d391..5b4c0a82bd1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.27.0 + 3.28.0-SNAPSHOT extra/pom.xml From 29db76776cc188ae2fda4d870c4c93aa3ac12f9c Mon Sep 17 00:00:00 2001 From: Jim Thario <33331284+JimTharioAmazon@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:40:22 -0700 Subject: [PATCH 29/51] Housekeeping: Move DB drivers to test scope (#4017) --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 5b4c0a82bd1..8afcb27fb36 100644 --- a/pom.xml +++ b/pom.xml @@ -163,10 +163,12 @@ com.mysql mysql-connector-j + test org.postgresql postgresql + test software.amazon.awssdk From 91e7db77918048d0ba00efc87dee7801a47be918 Mon Sep 17 00:00:00 2001 From: sindhuja-sridharan <148382298+sindhuja-sridharan@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:40:49 -0700 Subject: [PATCH 30/51] GumGum: Enable Opt-In change notification for GumGum Adapter (#4006) --- .github/workflows/scripts/codepath-notification | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scripts/codepath-notification b/.github/workflows/scripts/codepath-notification index d68419530de..001ccbebc1a 100644 --- a/.github/workflows/scripts/codepath-notification +++ b/.github/workflows/scripts/codepath-notification @@ -22,3 +22,4 @@ pubmatic|Pubmatic: header-bidding@pubmatic.com openx|OpenX: prebid@openx.com medianet|Medianet: prebid@media.net thetradedesk|TheTradeDesk: Prebid-Maintainers@thetradedesk.com +gumgum|GumGum: prebid@gumgum.com From d390d057aade56ec9b2e654049a381d9c40ca7b6 Mon Sep 17 00:00:00 2001 From: ShayanK16GumGum Date: Wed, 2 Jul 2025 01:11:18 +0530 Subject: [PATCH 31/51] GumGum: Collect the ad unit name for reporting (#3912) --- .../server/bidder/gumgum/GumgumBidder.java | 30 +++-- .../bidder/gumgum/GumgumBidderTest.java | 126 ++++++++++++++++++ 2 files changed, 148 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java index dbbca22ccfe..926af4ffbe6 100644 --- a/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java +++ b/src/main/java/org/prebid/server/bidder/gumgum/GumgumBidder.java @@ -25,6 +25,7 @@ import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgum; import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgumBanner; import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgumVideo; @@ -40,12 +41,13 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; public class GumgumBidder implements Bidder { private static final String REQUEST_EXT_PRODUCT = "product"; - private static final TypeReference> GUMGUM_EXT_TYPE_REFERENCE = + private static final TypeReference> GUMGUM_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -80,17 +82,24 @@ private BidRequest createBidRequest(BidRequest bidRequest, List err for (Imp imp : bidRequest.getImp()) { try { - final ExtImpGumgum extImp = parseImpExt(imp); - modifiedImps.add(modifyImp(imp, extImp)); + final ExtPrebid extImp = parseImpExt(imp); + final ExtImpGumgum extImpGumgum = extImp.getBidder(); + final String adUnitCode = Optional.ofNullable(extImp.getPrebid()) + .map(ExtImpPrebid::getAdUnitCode) + .orElse(null); - final String extZone = extImp.getZone(); + modifiedImps.add(modifyImp(imp, extImpGumgum, adUnitCode)); + + final String extZone = extImpGumgum.getZone(); if (StringUtils.isNotEmpty(extZone)) { zone = extZone; } - final BigInteger extPubId = extImp.getPubId(); + + final BigInteger extPubId = extImpGumgum.getPubId(); if (extPubId != null && !extPubId.equals(BigInteger.ZERO)) { pubId = extPubId; } + } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -106,15 +115,15 @@ private BidRequest createBidRequest(BidRequest bidRequest, List err .build(); } - private ExtImpGumgum parseImpExt(Imp imp) { + private ExtPrebid parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), GUMGUM_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), GUMGUM_EXT_TYPE_REFERENCE); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage()); } } - private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { + private Imp modifyImp(Imp imp, ExtImpGumgum extImp, String adUnitCode) { final Imp.ImpBuilder impBuilder = imp.toBuilder(); final String product = extImp.getProduct(); @@ -123,6 +132,10 @@ private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { impBuilder.ext(productExt); } + if (StringUtils.isNotEmpty(adUnitCode)) { + impBuilder.tagid(adUnitCode); + } + final Banner banner = imp.getBanner(); if (banner != null) { final Banner resolvedBanner = resolveBanner(banner, extImp); @@ -139,6 +152,7 @@ private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { impBuilder.video(resolvedVideo); } } + return impBuilder.build(); } diff --git a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java index 2595b6d903a..2b00840ed26 100644 --- a/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/gumgum/GumgumBidderTest.java @@ -21,12 +21,15 @@ 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.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgum; import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgumBanner; import org.prebid.server.proto.openrtb.ext.request.gumgum.ExtImpGumgumVideo; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -36,6 +39,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.tuple; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; @@ -45,6 +52,124 @@ public class GumgumBidderTest extends VertxTest { private final GumgumBidder target = new GumgumBidder(ENDPOINT_URL, jacksonMapper); + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(2) + .anySatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Cannot deserialize value"); + }); + } + + @Test + public void makeHttpRequestsShouldModifyImpressionsWhenValidInput() { + // given + final ObjectNode extImp = mapper.valueToTree(ExtPrebid.of( + ExtImpPrebid.builder().adUnitCode("adUnit123").build(), + ExtImpGumgum.of("zone", BigInteger.TEN, "irisId", null, "product"))); + + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.ext(extImp)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsExactly("adUnit123"); + } + + @Test + public void testMakeHttpRequestsShouldNotSetTagIdFromZoneWhenAdUnitIdIsMissing() throws IOException { + // given + final ObjectNode extImp = mapper.valueToTree(ExtPrebid.of(null, + ExtImpGumgum.of("zone123", BigInteger.TEN, "productA", null, "zone123"))); + + final Imp imp = Imp.builder() + .id("imp1") + .banner(Banner.builder().w(300).h(250).build()) + .ext(extImp) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .id("test-bid-request") + .imp(Collections.singletonList(imp)) + .site(Site.builder().id("test-site").build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertNotNull(result); + assertFalse(result.getValue().isEmpty()); + + final byte[] requestBody = result.getValue().get(0).getBody(); + final BidRequest modifiedRequest = mapper.readValue(requestBody, BidRequest.class); + + assertFalse(modifiedRequest.getImp().isEmpty()); + + final Imp modifiedImp = modifiedRequest.getImp().get(0); + + assertNull(modifiedImp.getTagid()); + assertEquals("test-site", modifiedRequest.getSite().getId(), "zone123"); + } + + @Test + public void makeHttpRequestsShouldReturnModifiedBidRequestWhenValidInput() { + // given + final ObjectNode extImp = mapper.valueToTree(ExtPrebid.of( + ExtImpPrebid.builder().adUnitCode("adUnit123").build(), + ExtImpGumgum.of("zone", BigInteger.TEN, "irisId", null, "product"))); + + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(extImp)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsExactly("adUnit123"); + } + + @Test + public void makeHttpRequestsShouldExtractAdUnitIdWhenPresent() { + // given + final ObjectNode extImp = mapper.valueToTree(ExtPrebid.of( + ExtImpPrebid.builder().adUnitCode("adUnit123").build(), + ExtImpGumgum.of("zone", BigInteger.TEN, "irisId", null, "product"))); + + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(extImp)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getTagid) + .containsExactly("adUnit123"); + } + @Test public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> new GumgumBidder("invalid_url", jacksonMapper)); @@ -422,4 +547,5 @@ private static BidderCall givenHttpCall(BidRequest bidRequest, Strin HttpResponse.of(200, null, body), null); } + } From 1822c1204a61b401da204dbf7a9d6ce31ffc2e54 Mon Sep 17 00:00:00 2001 From: tomaszbmf <142428312+tomaszbmf@users.noreply.github.com> Date: Tue, 1 Jul 2025 21:41:33 +0200 Subject: [PATCH 32/51] MobileFuse Adapter: Remove tagid_src and pub_id params (#3915) --- .../bidder/mobilefuse/MobilefuseBidder.java | 63 +++++++++---------- .../request/mobilefuse/ExtImpMobilefuse.java | 6 -- .../resources/bidder-config/mobilefuse.yaml | 2 +- .../static/bidder-params/mobilefuse.json | 11 +--- .../mobilefuse/MobilefuseBidderTest.java | 55 +++------------- .../org/prebid/server/it/MobilefuseTest.java | 2 +- .../test-auction-mobilefuse-request.json | 4 +- 7 files changed, 45 insertions(+), 98 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java b/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java index 5806faace29..40e5cf72074 100644 --- a/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java +++ b/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java @@ -23,6 +23,7 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -45,64 +46,62 @@ public MobilefuseBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final String endpoint = request.getImp().stream() - .map(this::parseImpExt) - .filter(Objects::nonNull) - .findFirst() - .map(this::makeUrl) - .orElse(null); - - if (endpoint == null) { - return Result.withError(BidderError.badInput("Invalid ExtImpMobilefuse value")); + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + if (!isValidImp(imp)) { + continue; + } + + final ExtImpMobilefuse extImp = parseImpExt(imp); + modifiedImps.add(modifyImp(imp, extImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } } - final List modifiedImps = request.getImp().stream() - .map(this::modifyImp) - .filter(Objects::nonNull) - .toList(); + if (!errors.isEmpty()) { + return Result.withErrors(errors); + } if (modifiedImps.isEmpty()) { return Result.withError(BidderError.badInput("No valid imps")); } final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); - return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, endpoint, mapper)); + return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, endpointUrl, mapper)); } - private Imp modifyImp(Imp imp) { - if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { - return null; - } - - final ExtImpMobilefuse impExt = parseImpExt(imp); - final ObjectNode skadn = parseSkadn(imp.getExt()); - return imp.toBuilder() - .tagid(Objects.toString(impExt != null ? impExt.getPlacementId() : null, "0")) - .ext(skadn != null ? mapper.mapper().createObjectNode().set(SKADN_PROPERTY_NAME, skadn) : null) - .build(); + private static boolean isValidImp(Imp imp) { + return imp.getBanner() != null || imp.getVideo() != null || imp.getXNative() != null; } private ExtImpMobilefuse parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), MOBILEFUSE_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - return null; + throw new PreBidException("Error parsing ExtImpMobilefuse value: %s".formatted(e.getMessage())); } } + private Imp modifyImp(Imp imp, ExtImpMobilefuse extImp) { + final ObjectNode skadn = parseSkadn(imp.getExt()); + return imp.toBuilder() + .tagid(Objects.toString(extImp.getPlacementId(), "0")) + .ext(skadn != null ? mapper.mapper().createObjectNode().set(SKADN_PROPERTY_NAME, skadn) : null) + .build(); + } + private ObjectNode parseSkadn(ObjectNode impExt) { try { return mapper.mapper().convertValue(impExt.get(SKADN_PROPERTY_NAME), ObjectNode.class); } catch (IllegalArgumentException e) { - return null; + throw new PreBidException(e.getMessage()); } } - private String makeUrl(ExtImpMobilefuse extImp) { - final String baseUrl = endpointUrl + Objects.toString(extImp.getPublisherId(), "0"); - return "ext".equals(extImp.getTagidSrc()) ? baseUrl + "&tagid_src=ext" : baseUrl; - } - @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java index a56f03baa2d..8e2b76d04a8 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java @@ -11,10 +11,4 @@ public class ExtImpMobilefuse { @JsonProperty("placement_id") Integer placementId; - - @JsonProperty("pub_id") - Integer publisherId; - - @JsonProperty("tagid_src") - String tagidSrc; } diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml index ea6645feb96..95b5ae28460 100644 --- a/src/main/resources/bidder-config/mobilefuse.yaml +++ b/src/main/resources/bidder-config/mobilefuse.yaml @@ -1,6 +1,6 @@ adapters: mobilefuse: - endpoint: http://mfx.mobilefuse.com/openrtb?pub_id= + endpoint: http://mfx.mobilefuse.com/openrtb ortb-version: "2.6" endpoint-compression: gzip # This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions: diff --git a/src/main/resources/static/bidder-params/mobilefuse.json b/src/main/resources/static/bidder-params/mobilefuse.json index aaab1aca295..5fe5d77c311 100644 --- a/src/main/resources/static/bidder-params/mobilefuse.json +++ b/src/main/resources/static/bidder-params/mobilefuse.json @@ -7,18 +7,9 @@ "placement_id": { "type": "integer", "description": "An ID which identifies this specific inventory placement" - }, - "pub_id": { - "type": "integer", - "description": "An ID which identifies the publisher selling the inventory." - }, - "tagid_src": { - "type": "string", - "description": "ext if passing publisher's ids, empty if passing MobileFuse IDs in placement_id field. Defaults to empty" } }, "required": [ - "placement_id", - "pub_id" + "placement_id" ] } diff --git a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java index 6de8d3f4d65..3f12e65af4e 100644 --- a/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidderTest.java @@ -36,7 +36,7 @@ public class MobilefuseBidderTest extends VertxTest { - private static final String ENDPOINT_URL = "https://test.endpoint.com/openrtb?pub_id="; + private static final String ENDPOINT_URL = "https://test.endpoint.com/openrtb"; private final MobilefuseBidder target = new MobilefuseBidder(ENDPOINT_URL, jacksonMapper); @@ -44,7 +44,7 @@ private static Imp givenImp(Function impCustomiz return impCustomizer.apply(Imp.builder() .id("imp_id") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpMobilefuse.of(1, 2, "tagidSrc"))))) + ExtImpMobilefuse.of(1))))) .build(); } @@ -84,15 +84,18 @@ public void makeHttpRequestsShouldReturnErrorIfNoValidExtFound() { // given final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder .id("456") - .banner(null) + .banner(Banner.builder().build()) .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); // when final Result>> result = target.makeHttpRequests(bidRequest); // then - assertThat(result.getErrors()) - .containsExactly(BidderError.badInput("Invalid ExtImpMobilefuse value")); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getMessage()).startsWith("Error parsing ExtImpMobilefuse value:"); + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + }); assertThat(result.getValue()).isEmpty(); } @@ -102,44 +105,6 @@ public void creationShouldFailOnInvalidEndpointUrl() { .isThrownBy(() -> new MobilefuseBidder("invalid_url", jacksonMapper)); } - @Test - public void makeHttpRequestsShouldCreateCorrectURLWhenTagidSrcEqualsExt() { - // given - final BidRequest bidRequest = givenBidRequest( - impBuilder -> impBuilder - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpMobilefuse.of(1, 2, "ext"))))); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.endpoint.com/openrtb?pub_id=2&tagid_src=ext"); - } - - @Test - public void makeHttpRequestsShouldSetPubIdToZeroIfPublisherIdNotPresentInRequest() { - // given - final BidRequest bidRequest = givenBidRequest( - impBuilder -> impBuilder - .banner(Banner.builder().build()) - .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpMobilefuse.of(1, null, null))))); - - // when - final Result>> result = target.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .extracting(HttpRequest::getUri) - .containsExactly("https://test.endpoint.com/openrtb?pub_id=0"); - } - @Test public void makeHttpRequestsShouldReturnErrorIfNoValidImpsFound() { // given @@ -193,7 +158,7 @@ public void makeHttpRequestsShouldModifyImpTagId() { .banner(Banner.builder().build()) .tagid("some tag id") .ext(mapper.valueToTree(ExtPrebid.of(null, - ExtImpMobilefuse.of(1, 2, "ext"))))); + ExtImpMobilefuse.of(1))))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -212,7 +177,7 @@ public void makeHttpRequestsShouldModifyImpWithAddingSkadnWhenSkadnIsPresent() { // given final ObjectNode skadn = mapper.createObjectNode().put("something", "something"); final ObjectNode impExt = mapper.createObjectNode(); - impExt.set("bidder", mapper.valueToTree(ExtImpMobilefuse.of(1, 2, "ext"))); + impExt.set("bidder", mapper.valueToTree(ExtImpMobilefuse.of(1))); impExt.set("skadn", skadn); final BidRequest bidRequest = givenBidRequest( impBuilder -> impBuilder.banner(Banner.builder().build()).ext(impExt)); diff --git a/src/test/java/org/prebid/server/it/MobilefuseTest.java b/src/test/java/org/prebid/server/it/MobilefuseTest.java index e732dce775e..83ab604f1f5 100644 --- a/src/test/java/org/prebid/server/it/MobilefuseTest.java +++ b/src/test/java/org/prebid/server/it/MobilefuseTest.java @@ -18,7 +18,7 @@ public class MobilefuseTest extends IntegrationTest { @Test public void openrtb2AuctionShouldRespondWithBidsFromMobilefuse() throws IOException, JSONException { // given - WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/mobilefuse-exchange/1111&tagid_src=ext")) + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/mobilefuse-exchange/")) .withRequestBody(equalToJson(jsonFrom("openrtb2/mobilefuse/test-mobilefuse-bid-request.json"))) .willReturn(aResponse().withBody(jsonFrom("openrtb2/mobilefuse/test-mobilefuse-bid-response.json")))); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json index 7a535f22be8..bc1bf97b9b1 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-request.json @@ -10,9 +10,7 @@ "tagid": "tag_id", "ext": { "mobilefuse": { - "placement_id": 999999, - "pub_id": 1111, - "tagid_src": "ext" + "placement_id": 999999 } } } From eb2e2646179547a6468db23cdd9c897a86788ddd Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:36:48 +0200 Subject: [PATCH 33/51] Kobler Adapter: Remove sensitive device and user data (#4043) --- .../server/bidder/kobler/KoblerBidder.java | 4 ++++ .../bidder/kobler/KoblerBidderTest.java | 21 +++++++++++++++++++ .../kobler/test-kobler-bid-request.json | 3 +-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java b/src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java index 7053432ba38..b87df9dcffa 100644 --- a/src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java +++ b/src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; @@ -77,9 +78,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ } } + final Device device = bidRequest.getDevice(); final BidRequest modifiedRequest = bidRequest.toBuilder() .imp(modifiedImps) .cur(normalizeCurrencies(bidRequest)) + .device(device != null ? device.toBuilder().ipv6(null).ip(null).build() : null) + .user(null) .build(); final String endpoint = isTest(imps.getFirst(), errors) ? devEndpoint : endpointUrl; diff --git a/src/test/java/org/prebid/server/bidder/kobler/KoblerBidderTest.java b/src/test/java/org/prebid/server/bidder/kobler/KoblerBidderTest.java index 63a8d3ec56c..fce7e9c2146 100644 --- a/src/test/java/org/prebid/server/bidder/kobler/KoblerBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/kobler/KoblerBidderTest.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -165,6 +167,25 @@ public void makeHttpRequestsShouldAddUsdToCurrenciesIfMissing() { .containsExactly(List.of("EUR", "USD")); } + @Test + public void makeHttpRequestsShouldSanitizeDeviceAndUserData() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request + .device(Device.builder().ip("ip").ipv6("ipv6").ua("ua").build()) + .user(User.builder().consent("consent").build()), + givenImp(identity())); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .extracting(BidRequest::getDevice, BidRequest::getUser) + .containsExactly(tuple(Device.builder().ua("ua").build(), null)); + } + @Test public void makeBidsShouldReturnErrorIfResponseBodyIsInvalid() { // given diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kobler/test-kobler-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/kobler/test-kobler-bid-request.json index 83770158d3d..95d78be2e70 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/kobler/test-kobler-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/kobler/test-kobler-bid-request.json @@ -27,8 +27,7 @@ } }, "device": { - "ua": "userAgent", - "ip": "193.168.244.1" + "ua": "userAgent" }, "at": 1, "tmax": "${json-unit.any-number}", From eb2fef614959ffd367461ca45b321f4c27d9f699 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:36:57 +0200 Subject: [PATCH 34/51] Gothamads Adapter: New Intenze Alias (#4042) --- .../resources/bidder-config/gothamads.yaml | 6 +++ .../org/prebid/server/it/IntenzeTest.java | 35 +++++++++++++ .../intenze/test-auction-intenze-request.json | 23 +++++++++ .../test-auction-intenze-response.json | 40 +++++++++++++++ .../intenze/test-intenze-bid-request.json | 50 +++++++++++++++++++ .../intenze/test-intenze-bid-response.json | 19 +++++++ .../server/it/test-application.properties | 2 + 7 files changed, 175 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/IntenzeTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-response.json diff --git a/src/main/resources/bidder-config/gothamads.yaml b/src/main/resources/bidder-config/gothamads.yaml index bd1e2120945..eccdefb17e8 100644 --- a/src/main/resources/bidder-config/gothamads.yaml +++ b/src/main/resources/bidder-config/gothamads.yaml @@ -1,6 +1,12 @@ adapters: gothamads: endpoint: http://us-e-node1.gothamads.com/?pass={{AccountID}} + aliases: + intenze: + enabled: false + endpoint: http://lb-east.intenze.co/?pass={{AccountID}} + meta-info: + maintainer-email: connect@intenze.co meta-info: maintainer-email: support@gothamads.com app-media-types: diff --git a/src/test/java/org/prebid/server/it/IntenzeTest.java b/src/test/java/org/prebid/server/it/IntenzeTest.java new file mode 100644 index 00000000000..bd662312fae --- /dev/null +++ b/src/test/java/org/prebid/server/it/IntenzeTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class IntenzeTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromIntenze() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/intenze-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/intenze/test-intenze-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/intenze/test-intenze-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/intenze/test-auction-intenze-request.json", + Endpoint.openrtb2_auction + ); + + // then + assertJsonEquals("openrtb2/intenze/test-auction-intenze-response.json", response, List.of("intenze")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-request.json b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-request.json new file mode 100644 index 00000000000..9099ff0a2b3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "intenze": { + "accountId": "accountid" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-response.json b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-response.json new file mode 100644 index 00000000000..cbbb057c5e5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-auction-intenze-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "intenze" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "intenze", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "intenze": "{{ intenze.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-request.json new file mode 100644 index 00000000000..7c09fbd33f7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-request.json @@ -0,0 +1,50 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + } + } + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "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/intenze/test-intenze-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-response.json new file mode 100644 index 00000000000..47d4f8718ea --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/intenze/test-intenze-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048", + "mtype": 1 + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 717b0c09445..6771bcf6362 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -244,6 +244,8 @@ adapters.globalsun.enabled=true adapters.globalsun.endpoint=http://localhost:8090/globalsun-exchange adapters.gothamads.enabled=true adapters.gothamads.endpoint=http://localhost:8090/gothamads-exchange +adapters.gothamads.aliases.intenze.enabled=true +adapters.gothamads.aliases.intenze.endpoint=http://localhost:8090/intenze-exchange adapters.grid.enabled=true adapters.grid.endpoint=http://localhost:8090/grid-exchange adapters.gumgum.enabled=true From 74143c6d205d3564846fe7fc2e286640ab9445cc Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:37:14 +0200 Subject: [PATCH 35/51] Rubicon: Remove default maxbids value (#4041) --- .../org/prebid/server/bidder/rubicon/RubiconBidder.java | 5 ++--- .../prebid/server/bidder/rubicon/RubiconBidderTest.java | 7 ++----- .../it/openrtb2/magnite/test-magnite-bid-request.json | 1 - .../it/openrtb2/rubicon/test-rubicon-bid-request.json | 1 - 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java index d66fd90adf5..5760b9f4242 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -890,15 +890,14 @@ private List mapVendorsNamesToUrls(List metrics) { return vendorsUrls.isEmpty() ? null : vendorsUrls; } - private Integer getMaxBids(ExtRequest extRequest) { + private static Integer getMaxBids(ExtRequest extRequest) { final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; final List multibids = extRequestPrebid != null ? extRequestPrebid.getMultibid() : null; final ExtRequestPrebidMultiBid extRequestPrebidMultiBid = CollectionUtils.isNotEmpty(multibids) ? multibids.getFirst() : null; - final Integer multibidMaxBids = extRequestPrebidMultiBid != null ? extRequestPrebidMultiBid.getMaxBids() : null; - return multibidMaxBids != null ? multibidMaxBids : 1; + return extRequestPrebidMultiBid != null ? extRequestPrebidMultiBid.getMaxBids() : null; } private String getGpid(ObjectNode impExt) { diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java index 49cb2a20e97..d85e94191a6 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -599,7 +599,7 @@ public void makeHttpRequestsShouldPassNativeBattrField() { } @Test - public void makeHttpRequestsShouldAddMaxbidsAttributeAsOneIfExtPrebidMultibidMaxBidsIsNotPresent() { + public void makeHttpRequestsShouldAddMaxbidsAttributeAsNullIfExtPrebidMultibidMaxBidsIsNotPresent() { // given final BidRequest bidRequest = givenBidRequest( builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() @@ -620,7 +620,7 @@ public void makeHttpRequestsShouldAddMaxbidsAttributeAsOneIfExtPrebidMultibidMax .extracting(Imp::getExt).doesNotContainNull() .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)) .extracting(RubiconImpExt::getMaxbids) - .containsExactly(1); + .containsOnlyNulls(); } @Test @@ -659,7 +659,6 @@ public void makeHttpRequestsShouldFillImpExt() { null, "uuid_bid_id")) .skadn(givenSkadn) - .maxbids(1) .build()); } @@ -2445,7 +2444,6 @@ public void makeHttpRequestsShouldCreateRequestPerImp() { .video(Video.builder().build()) .ext(mapper.valueToTree(RubiconImpExt.builder() .rp(expectedImpExtRp) - .maxbids(1) .build())) .build())) .build(); @@ -2456,7 +2454,6 @@ public void makeHttpRequestsShouldCreateRequestPerImp() { .ext(mapper.valueToTree( RubiconImpExt.builder() .rp(expectedImpExtRp) - .maxbids(1) .build())) .build())) .build(); diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json index cb9ea650de8..37492413f7b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json @@ -28,7 +28,6 @@ "mint_version": "" } }, - "maxbids": 1, "tid": "${json-unit.any-string}" } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json index cb9ea650de8..37492413f7b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json @@ -28,7 +28,6 @@ "mint_version": "" } }, - "maxbids": 1, "tid": "${json-unit.any-string}" } } From 3fdc4d4993b9e0876f7a5583c6333b5b9a5ba0fd Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:37:27 +0200 Subject: [PATCH 36/51] Dianomi Adapter: Update user syncs to send gdpr_consent (#4022) --- src/main/resources/bidder-config/dianomi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/bidder-config/dianomi.yaml b/src/main/resources/bidder-config/dianomi.yaml index b0377810a72..6b36d1c29ab 100644 --- a/src/main/resources/bidder-config/dianomi.yaml +++ b/src/main/resources/bidder-config/dianomi.yaml @@ -16,10 +16,10 @@ adapters: usersync: cookie-family-name: dianomi iframe: - url: https://www-prebid.dianomi.com/prebid/usersync/index.html?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://www-prebid.dianomi.com/prebid/usersync/index.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false uid-macro: '$UID' redirect: - url: https://data.dianomi.com/frontend/usync?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://data.dianomi.com/frontend/usync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false uid-macro: '$UID' From 81bd38f73fd2d524c6795d1daafe62f8cf0f4739 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:37:37 +0200 Subject: [PATCH 37/51] Admatic Alias: Netaddiction (#4018) --- src/main/resources/bidder-config/admatic.yaml | 3 + .../prebid/server/it/NetAddictionTest.java | 41 +++++++++++++ .../test-auction-netaddiction-request.json | 24 ++++++++ .../test-auction-netaddiction-response.json | 39 +++++++++++++ .../test-netaddiction-bid-request.json | 57 +++++++++++++++++++ .../test-netaddiction-bid-response.json | 18 ++++++ .../server/it/test-application.properties | 2 + 7 files changed, 184 insertions(+) create mode 100644 src/test/java/org/prebid/server/it/NetAddictionTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-response.json diff --git a/src/main/resources/bidder-config/admatic.yaml b/src/main/resources/bidder-config/admatic.yaml index 1ba10be3b32..9c0ac6a483e 100644 --- a/src/main/resources/bidder-config/admatic.yaml +++ b/src/main/resources/bidder-config/admatic.yaml @@ -21,6 +21,9 @@ adapters: enabled: false meta-info: maintainer-email: adops@yobee.it + netaddiction: + meta-info: + maintainer-email: publishers-support@netaddiction.it meta-info: maintainer-email: prebid@admatic.com.tr app-media-types: diff --git a/src/test/java/org/prebid/server/it/NetAddictionTest.java b/src/test/java/org/prebid/server/it/NetAddictionTest.java new file mode 100644 index 00000000000..d9f1a815820 --- /dev/null +++ b/src/test/java/org/prebid/server/it/NetAddictionTest.java @@ -0,0 +1,41 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class NetAddictionTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromNetaddiction() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/netaddiction-exchange")) + .withQueryParam("host", equalTo("host")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/netaddiction/test-netaddiction-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/netaddiction/test-netaddiction-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/netaddiction/test-auction-netaddiction-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/netaddiction/test-auction-netaddiction-response.json", + response, + List.of("netaddiction")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-request.json b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-request.json new file mode 100644 index 00000000000..232939b65c0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-request.json @@ -0,0 +1,24 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "netaddiction": { + "host": "host", + "networkId": 4 + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-response.json new file mode 100644 index 00000000000..8e1acafd34b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-auction-netaddiction-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "netaddiction" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "netaddiction", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "netaddiction": "{{ netaddiction.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-request.json new file mode 100644 index 00000000000..806fd172232 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-request.json @@ -0,0 +1,57 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "host": "host", + "networkId": 4 + } + } + } + ], + "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/netaddiction/test-netaddiction-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-response.json new file mode 100644 index 00000000000..da5c7fc51cd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/netaddiction/test-netaddiction-bid-response.json @@ -0,0 +1,18 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 6771bcf6362..a37068f2c93 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -47,6 +47,8 @@ adapters.admatic.aliases.admaticde.enabled=true adapters.admatic.aliases.admaticde.endpoint=http://localhost:8090/admaticde-exchange?host={{Host}} adapters.admatic.aliases.yobee.enabled=true adapters.admatic.aliases.yobee.endpoint=http://localhost:8090/yobee-exchange?host={{Host}} +adapters.admatic.aliases.netaddiction.enabled=true +adapters.admatic.aliases.netaddiction.endpoint=http://localhost:8090/netaddiction-exchange?host={{Host}} adapters.admixer.enabled=true adapters.admixer.endpoint=http://localhost:8090/admixer-exchange adapters.adnuntius.enabled=true From a30106684e1ed5e15c4638d95c48d46108587455 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:38:02 +0200 Subject: [PATCH 38/51] Colossus Adapter: Declare OpenRTB 2.6 support (#4014) --- src/main/resources/bidder-config/colossus.yaml | 1 + src/test/java/org/prebid/server/it/ColossussspTest.java | 8 ++++---- .../openrtb2/colossus/test-auction-colossus-request.json | 4 +--- .../it/openrtb2/colossus/test-colossus-bid-request.json | 4 +--- .../test-auction-colossusssp-request.json | 4 +--- .../test-auction-colossusssp-response.json | 0 .../test-colossusssp-bid-request.json | 4 +--- .../test-colossusssp-bid-response.json | 0 8 files changed, 9 insertions(+), 16 deletions(-) rename src/test/resources/org/prebid/server/it/openrtb2/{colossus/aliases => colossusssp}/test-auction-colossusssp-request.json (87%) rename src/test/resources/org/prebid/server/it/openrtb2/{colossus/aliases => colossusssp}/test-auction-colossusssp-response.json (100%) rename src/test/resources/org/prebid/server/it/openrtb2/{colossus/aliases => colossusssp}/test-colossusssp-bid-request.json (96%) rename src/test/resources/org/prebid/server/it/openrtb2/{colossus/aliases => colossusssp}/test-colossusssp-bid-response.json (100%) diff --git a/src/main/resources/bidder-config/colossus.yaml b/src/main/resources/bidder-config/colossus.yaml index 8e5bf6632d4..9514160b5a2 100644 --- a/src/main/resources/bidder-config/colossus.yaml +++ b/src/main/resources/bidder-config/colossus.yaml @@ -1,6 +1,7 @@ adapters: colossus: endpoint: http://colossusssp.com/?c=o&m=rtb + ortb-version: "2.6" aliases: colossusssp: enabled: false diff --git a/src/test/java/org/prebid/server/it/ColossussspTest.java b/src/test/java/org/prebid/server/it/ColossussspTest.java index a84d9799c41..98826e2702b 100644 --- a/src/test/java/org/prebid/server/it/ColossussspTest.java +++ b/src/test/java/org/prebid/server/it/ColossussspTest.java @@ -19,16 +19,16 @@ public class ColossussspTest extends IntegrationTest { public void openrtb2AuctionShouldRespondWithBidsFromColossusssp() throws IOException, JSONException { // given WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/colossusssp-exchange")) - .withRequestBody(equalToJson(jsonFrom("openrtb2/colossus/aliases/test-colossusssp-bid-request.json"))) + .withRequestBody(equalToJson(jsonFrom("openrtb2/colossusssp/test-colossusssp-bid-request.json"))) .willReturn(aResponse().withBody( - jsonFrom("openrtb2/colossus/aliases/test-colossusssp-bid-response.json")))); + jsonFrom("openrtb2/colossusssp/test-colossusssp-bid-response.json")))); // when - final Response response = responseFor("openrtb2/colossus/aliases/test-auction-colossusssp-request.json", + final Response response = responseFor("openrtb2/colossusssp/test-auction-colossusssp-request.json", Endpoint.openrtb2_auction); // then - assertJsonEquals("openrtb2/colossus/aliases/test-auction-colossusssp-response.json", response, + assertJsonEquals("openrtb2/colossusssp/test-auction-colossusssp-response.json", response, singletonList("colossusssp")); } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-request.json index 47047949eba..133c994bbbc 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-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/colossus/test-colossus-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-colossus-bid-request.json index f41eed66c6a..b93c5ddcb0b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-colossus-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-colossus-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/colossus/aliases/test-auction-colossusssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-auction-colossusssp-request.json similarity index 87% rename from src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-auction-colossusssp-request.json index bec0c44a62a..a5a940a755a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-auction-colossusssp-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/colossus/aliases/test-auction-colossusssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-auction-colossusssp-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-auction-colossusssp-response.json diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-colossusssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-colossusssp-bid-request.json similarity index 96% rename from src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-colossusssp-bid-request.json rename to src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-colossusssp-bid-request.json index f41eed66c6a..b93c5ddcb0b 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-colossusssp-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-colossusssp-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/colossus/aliases/test-colossusssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-colossusssp-bid-response.json similarity index 100% rename from src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-colossusssp-bid-response.json rename to src/test/resources/org/prebid/server/it/openrtb2/colossusssp/test-colossusssp-bid-response.json From 029d85934699d1567da3759e8c05a0d139479d47 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:38:25 +0200 Subject: [PATCH 39/51] Pubmatic Adapter: Enable Gzip compression (#4013) --- src/main/resources/bidder-config/pubmatic.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/bidder-config/pubmatic.yaml b/src/main/resources/bidder-config/pubmatic.yaml index 5ca8ad94339..81f9b89f6a1 100644 --- a/src/main/resources/bidder-config/pubmatic.yaml +++ b/src/main/resources/bidder-config/pubmatic.yaml @@ -1,6 +1,7 @@ adapters: pubmatic: endpoint: https://hbopenbid.pubmatic.com/translator?source=prebid-server + endpoint-compression: gzip ortb-version: "2.6" meta-info: maintainer-email: header-bidding@pubmatic.com From 699dbd5a94869fae17e8e241abc3412f4e42be26 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:38:55 +0200 Subject: [PATCH 40/51] Adport & Bidsmind Adapters: Change user sync urls (#4012) --- src/main/resources/bidder-config/adverxo.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/bidder-config/adverxo.yaml b/src/main/resources/bidder-config/adverxo.yaml index 8474c269505..d2fd18f505b 100644 --- a/src/main/resources/bidder-config/adverxo.yaml +++ b/src/main/resources/bidder-config/adverxo.yaml @@ -10,11 +10,11 @@ adapters: enabled: false cookie-family-name: adport iframe: - url: https://adport.pbsadverxo.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://cittamatra.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} uid-macro: '$UID' support-cors: false redirect: - url: https://adport.pbsadverxo.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://cittamatra.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} uid-macro: '$UID' support-cors: false bidsmind: @@ -24,11 +24,11 @@ adapters: enabled: false cookie-family-name: bidsmind iframe: - url: https://bidsmind.pbsadverxo.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://taetee.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} uid-macro: '$UID' support-cors: false redirect: - url: https://bidsmind.pbsadverxo.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://taetee.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} uid-macro: '$UID' support-cors: false mobupps: From 9d074347f907d1f14a937bc926a458e426002ba6 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:39:02 +0200 Subject: [PATCH 41/51] MobileFuse Adapter: Add usersync info (#4011) --- src/main/resources/bidder-config/mobilefuse.yaml | 10 ++++++++++ src/main/resources/bidder-config/nextmillennium.yaml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/resources/bidder-config/mobilefuse.yaml b/src/main/resources/bidder-config/mobilefuse.yaml index 95b5ae28460..0942ce43ef9 100644 --- a/src/main/resources/bidder-config/mobilefuse.yaml +++ b/src/main/resources/bidder-config/mobilefuse.yaml @@ -16,3 +16,13 @@ adapters: site-media-types: supported-vendors: vendor-id: 909 + usersync: + cookie-family-name: mobilefuse + iframe: + url: https://mfx.mobilefuse.com/usync?us_privacy={{us_privacy}}&pxurl={{redirect_url}} + support-cors: false + uid-macro: '$UID' + redirect: + url: https://mfx.mobilefuse.com/getuid?us_privacy={{us_privacy}}&redir={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/nextmillennium.yaml b/src/main/resources/bidder-config/nextmillennium.yaml index a66930c248b..67c34ae62d4 100644 --- a/src/main/resources/bidder-config/nextmillennium.yaml +++ b/src/main/resources/bidder-config/nextmillennium.yaml @@ -21,4 +21,4 @@ adapters: redirect: url: https://cookies.nextmillmedia.com/sync?type=image&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false - userMacro: '[NMUID]' + uid-macro: '[NMUID]' From 51658c44a4f720511797608cc209d577753c3bb1 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Wed, 9 Jul 2025 12:34:07 +0300 Subject: [PATCH 42/51] Core: Add toggle to enable round-robin inet address selection of the ip address to use (#4048) --- docs/config-app.md | 1 + .../server/spring/config/VertxConfiguration.java | 11 +++++++++-- src/main/resources/application.yaml | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/config-app.md b/docs/config-app.md index ca0f53e51a0..52c454075a6 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -14,6 +14,7 @@ This section can be extended against standard [Spring configuration](https://doc This parameter exists to allow to change the location of the directory Vert.x will create because it will and there is no way to make it not. - `vertx.init-timeout-ms` - time to wait for asynchronous initialization steps completion before considering them stuck. When exceeded - exception is thrown and Prebid Server stops. - `vertx.enable-per-client-endpoint-metrics` - enables HTTP client metrics per destination endpoint (`host:port`) +- `vertx.round-robin-inet-address` - enables round-robin inet address selection of the ip address to use ## Server - `server.max-headers-size` - set the maximum length of all headers. diff --git a/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java b/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java index 9b5250a4233..3ac62c8fd59 100644 --- a/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/VertxConfiguration.java @@ -2,6 +2,7 @@ import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; +import io.vertx.core.dns.AddressResolverOptions; import io.vertx.core.file.FileSystem; import io.vertx.ext.dropwizard.DropwizardMetricsOptions; import io.vertx.ext.dropwizard.Match; @@ -23,7 +24,9 @@ public class VertxConfiguration { @Bean Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize, @Value("${vertx.enable-per-client-endpoint-metrics}") boolean enablePerClientEndpointMetrics, - @Value("${metrics.jmx.enabled}") boolean jmxEnabled) { + @Value("${metrics.jmx.enabled}") boolean jmxEnabled, + @Value("${vertx.round-robin-inet-address}") boolean roundRobinInetAddress) { + final DropwizardMetricsOptions metricsOptions = new DropwizardMetricsOptions() .setEnabled(true) .setJmxEnabled(jmxEnabled) @@ -32,10 +35,14 @@ Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize, metricsOptions.addMonitoredHttpClientEndpoint(new Match().setValue(".*").setType(MatchType.REGEX)); } + final AddressResolverOptions addressResolverOptions = new AddressResolverOptions(); + addressResolverOptions.setRoundRobinInetAddress(roundRobinInetAddress); + final VertxOptions vertxOptions = new VertxOptions() .setPreferNativeTransport(true) .setWorkerPoolSize(workerPoolSize) - .setMetricsOptions(metricsOptions); + .setMetricsOptions(metricsOptions) + .setAddressResolverOptions(addressResolverOptions); final Vertx vertx = Vertx.vertx(vertxOptions); logger.info("Native transport enabled: {}", vertx.isNativeTransportEnabled()); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 64ce4e516a4..f52895024d2 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,6 +6,7 @@ vertx: uploads-dir: file-uploads init-timeout-ms: 5000 enable-per-client-endpoint-metrics: false + round-robin-inet-address: false server: max-initial-line-length: 8092 max-headers-size: 16384 From 7c646d97bd59abff9c29cb2040d71c2f7fdee49a Mon Sep 17 00:00:00 2001 From: kim-ng93 <76963037+kim-ng93@users.noreply.github.com> Date: Wed, 9 Jul 2025 05:03:46 -0700 Subject: [PATCH 43/51] Inmobi: Port usersync redirect (#4029) --- src/main/resources/bidder-config/inmobi.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/bidder-config/inmobi.yaml b/src/main/resources/bidder-config/inmobi.yaml index 2107d52524a..2ace60bffff 100644 --- a/src/main/resources/bidder-config/inmobi.yaml +++ b/src/main/resources/bidder-config/inmobi.yaml @@ -19,3 +19,7 @@ adapters: url: https://sync.inmobi.com/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false uid-macro: '{ID5UID}' + redirect: + url: https://sync.inmobi.com/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + userMacro: '{ID5UID}' From 2d2d3398b8ecc82d7e902492181cecc63793a064 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:06:52 +0200 Subject: [PATCH 44/51] FreewheelSSP: new alias fwssp (#4019) --- .../freewheelssp/ExtImpFreewheelSSP.java | 6 ++ .../resources/bidder-config/freewheelssp.yaml | 5 ++ .../resources/static/bidder-params/fwssp.json | 25 +++++++++ .../freewheelssp/FreewheelSSPBidderTest.java | 12 ++-- .../java/org/prebid/server/it/FwsspTest.java | 35 ++++++++++++ .../fwssp/test-auction-fwssp-request.json | 29 ++++++++++ .../fwssp/test-auction-fwssp-response.json | 40 +++++++++++++ .../fwssp/test-fwssp-bid-request.json | 56 +++++++++++++++++++ .../fwssp/test-fwssp-bid-response.json | 19 +++++++ .../server/it/test-application.properties | 2 + 10 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/static/bidder-params/fwssp.json create mode 100644 src/test/java/org/prebid/server/it/FwsspTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-response.json diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java index eb297969465..f5d0276f429 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java @@ -8,4 +8,10 @@ public class ExtImpFreewheelSSP { @JsonProperty("zoneId") String zoneId; + + String customSiteSectionId; + + String networkId; + + String profileId; } diff --git a/src/main/resources/bidder-config/freewheelssp.yaml b/src/main/resources/bidder-config/freewheelssp.yaml index b198a837a9c..d657e641adc 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -3,6 +3,11 @@ adapters: endpoint: https://ads.stickyadstv.com/openrtb/dsp ortb-version: "2.6" modifying-vast-xml-allowed: true + aliases: + fwssp: + enabled: false + endpoint: "https://prebid.v.fwmrm.net/ortb/ssp" + endpoint-compression: gzip meta-info: maintainer-email: prebid-maintainer@freewheel.com app-media-types: diff --git a/src/main/resources/static/bidder-params/fwssp.json b/src/main/resources/static/bidder-params/fwssp.json new file mode 100644 index 00000000000..8c791621e76 --- /dev/null +++ b/src/main/resources/static/bidder-params/fwssp.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "FWSSP Adapter Params", + "description": "A schema which validates params accepted by the FWSSP adapter", + "type": "object", + "properties": { + "custom_site_section_id": { + "type": "string", + "description": "custom Site Section tag (e.g. ss_12345) or numeric Site Section ID (e.g. 12345)" + }, + "network_id": { + "type": "string", + "description": "Network ID (e.g. 12345)" + }, + "profile_id": { + "type": "string", + "description": "The value should contain a profile name. and NOT a numeric profile ID. This can either include the network ID prefix (e.g. 123456:profile_name_xyz123) or with the profile name alone (e.g. profile_name_xyz123)" + } + }, + "required": [ + "custom_site_section_id", + "network_id", + "profile_id" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidderTest.java b/src/test/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidderTest.java index ee16abe138f..ca50d10078b 100644 --- a/src/test/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/freewheelssp/FreewheelSSPBidderTest.java @@ -73,13 +73,16 @@ public void makeHttpRequestsShouldModifyImps() { final Result>> result = target.makeHttpRequests(bidRequest); // then + final Map expectedImpExt = Map.of( + "zoneId", "zoneId", + "custom_site_section_id", "customSiteSectionId", + "network_id", "networkId", + "profile_id", "profileId"); assertThat(result.getValue()) .extracting(HttpRequest::getPayload) .flatExtracting(BidRequest::getImp) .extracting(Imp::getExt) - .containsExactly( - mapper.valueToTree(Map.of("zoneId", "1")), - mapper.valueToTree(Map.of("zoneId", "1"))); + .containsExactly(mapper.valueToTree(expectedImpExt), mapper.valueToTree(expectedImpExt)); assertThat(result.getErrors()).isEmpty(); } @@ -197,7 +200,8 @@ private static BidRequest givenBidRequest(Imp... imps) { private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id("123") - .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpFreewheelSSP.of("1"))))) + .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpFreewheelSSP.of( + "zoneId", "customSiteSectionId", "networkId", "profileId"))))) .build(); } diff --git a/src/test/java/org/prebid/server/it/FwsspTest.java b/src/test/java/org/prebid/server/it/FwsspTest.java new file mode 100644 index 00000000000..45bba896a06 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FwsspTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singletonList; + +public class FwsspTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromFwssp() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/fwssp-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/fwssp/test-fwssp-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/fwssp/test-fwssp-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/fwssp/test-auction-fwssp-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/fwssp/test-auction-fwssp-response.json", response, + singletonList("fwssp")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-request.json b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-request.json new file mode 100644 index 00000000000..e0feff74147 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-request.json @@ -0,0 +1,29 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp-1", + "video": { + "mimes": ["video/mp4"], + "w": 300, + "h": 250 + }, + "ext": { + "fwssp": { + "profile_id": "123", + "network_id": "456", + "custom_site_section_id": "789" + } + } + } + ], + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-response.json new file mode 100644 index 00000000000..02ac48d5c2c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-auction-fwssp-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "12345_fwssp-test_1", + "impid": "imp-1", + "exp": 1500, + "price": 1.0, + "adid": "7857", + "adm": "", + "cid": "4001", + "crid": "7857", + "ext": { + "prebid": { + "type": "video", + "meta": { + "adaptercode": "fwssp" + } + }, + "origbidcpm": 1 + } + } + ], + "seat": "fwssp", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "fwssp": "{{ fwssp.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-request.json new file mode 100644 index 00000000000..b78419009fc --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp-1", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 300, + "h": 250 + }, + "secure": 1, + "ext": { + "profile_id": "123", + "network_id": "456", + "custom_site_section_id": "789" + } + } + ], + "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" : { + "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/fwssp/test-fwssp-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-response.json new file mode 100644 index 00000000000..4ad4c804c45 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/fwssp/test-fwssp-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "12345_fwssp-test_1", + "impid": "imp-1", + "price": 1.0, + "adid": "7857", + "adm": "", + "cid": "4001", + "crid": "7857" + } + ], + "type": "video" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index a37068f2c93..f3c83c787a7 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -236,6 +236,8 @@ adapters.audiencenetwork.platform-id=101 adapters.audiencenetwork.app-secret=67234 adapters.freewheelssp.enabled=true adapters.freewheelssp.endpoint=http://localhost:8090/freewheelssp-exchange +adapters.freewheelssp.aliases.fwssp.enabled=true +adapters.freewheelssp.aliases.fwssp.endpoint=http://localhost:8090/fwssp-exchange adapters.frvradn.enabled=true adapters.frvradn.endpoint=http://localhost:8090/frvradn-exchange adapters.gamma.enabled=true From 50a74bb8c61f24e5bc6c3efba6412686f6a983a5 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:27:46 +0200 Subject: [PATCH 45/51] New Adagio Adapter (#4027) --- .../server/bidder/adagio/AdagioBidder.java | 101 ++++++++ .../ext/request/adagio/ExtImpAdagio.java | 17 ++ .../config/bidder/AdagioConfiguration.java | 41 +++ src/main/resources/bidder-config/adagio.yaml | 22 ++ .../static/bidder-params/adagio.json | 31 +++ .../bidder/adagio/AdagioBidderTest.java | 245 ++++++++++++++++++ .../java/org/prebid/server/it/AdagioTest.java | 34 +++ .../adagio/test-adagio-bid-request.json | 55 ++++ .../adagio/test-adagio-bid-response.json | 21 ++ .../adagio/test-auction-adagio-request.json | 22 ++ .../adagio/test-auction-adagio-response.json | 43 +++ .../server/it/test-application.properties | 2 + 12 files changed, 634 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/adagio/AdagioBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AdagioConfiguration.java create mode 100644 src/main/resources/bidder-config/adagio.yaml create mode 100644 src/main/resources/static/bidder-params/adagio.json create mode 100644 src/test/java/org/prebid/server/bidder/adagio/AdagioBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AdagioTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-response.json diff --git a/src/main/java/org/prebid/server/bidder/adagio/AdagioBidder.java b/src/main/java/org/prebid/server/bidder/adagio/AdagioBidder.java new file mode 100644 index 00000000000..460d6ac2e4e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adagio/AdagioBidder.java @@ -0,0 +1,101 @@ +package org.prebid.server.bidder.adagio; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class AdagioBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdagioBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + return Result.withValue(BidderUtil.defaultRequest(request, endpointUrl, mapper)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + errors.add(BidderError.badServerResponse("empty seatbid array")); + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .flatMap(seatBid -> seatBid.getBid().stream() + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), seatBid.getSeat(), errors))) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, String seat, List errors) { + try { + final ExtBidPrebid extBidPrebid = parseBidExt(bid); + return BidderBid.builder() + .bid(bid) + .type(getBidType(bid)) + .bidCurrency(currency) + .videoInfo(extBidPrebid != null ? extBidPrebid.getVideo() : null) + .seat(seat) + .build(); + + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private ExtBidPrebid parseBidExt(Bid bid) { + try { + return mapper.mapper().convertValue(bid.getExt(), ExtBidPrebid.class); + } catch (IllegalArgumentException e) { + throw new PreBidException("bid.ext can not be parsed"); + } + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException( + "Could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java new file mode 100644 index 00000000000..37db89821b5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.adagio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdagio { + + @JsonProperty("organizationId") + String organizationId; + + String placement; + + String pagetype; + + String category; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdagioConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdagioConfiguration.java new file mode 100644 index 00000000000..cd4544370a8 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdagioConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adagio.AdagioBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/adagio.yaml", factory = YamlPropertySourceFactory.class) +public class AdagioConfiguration { + + private static final String BIDDER_NAME = "adagio"; + + @Bean("adagioConfigurationProperties") + @ConfigurationProperties("adapters.adagio") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adagioBidderDeps(BidderConfigurationProperties adagioConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adagioConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdagioBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/adagio.yaml b/src/main/resources/bidder-config/adagio.yaml new file mode 100644 index 00000000000..c252b39c5af --- /dev/null +++ b/src/main/resources/bidder-config/adagio.yaml @@ -0,0 +1,22 @@ +adapters: + adagio: + # Please deploy this config in each of your datacenters with the appropriate regional subdomain. + # Replace the `REGION` by one of the value below: + # - For AMER: las => (https://mp-las.4dex.io/pbserver) + # - For EMEA: ams => (https://mp-ams.4dex.io/pbserver) + # - For APAC: tyo => (https://mp-tyo.4dex.io/pbserver) + endpoint: https://mp-REGION.4dex.io/pbserver + ortb-version: "2.6" + endpoint-compression: gzip + meta-info: + maintainer-email: dev@adagio.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 617 diff --git a/src/main/resources/static/bidder-params/adagio.json b/src/main/resources/static/bidder-params/adagio.json new file mode 100644 index 00000000000..bacb62125ca --- /dev/null +++ b/src/main/resources/static/bidder-params/adagio.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adagio Adapter Params", + "description": "A schema which validates params accepted by the Adagio adapter", + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "description": "Id of the Organization. Handed out by Adagio." + }, + "placement": { + "type": "string", + "description": "Refers to the placement of an adunit in a page. Must not contain any information about the type of device.", + "maxLength": 30 + }, + "pagetype": { + "type": "string", + "description": "Describes what kind of content will be present in the page.", + "maxLength": 30 + }, + "category": { + "type": "string", + "description": "Category of the content displayed in the page.", + "maxLength": 30 + } + }, + "required": [ + "organizationId", + "placement" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/adagio/AdagioBidderTest.java b/src/test/java/org/prebid/server/bidder/adagio/AdagioBidderTest.java new file mode 100644 index 00000000000..e4b7440f6cd --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/adagio/AdagioBidderTest.java @@ -0,0 +1,245 @@ +package org.prebid.server.bidder.adagio; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class AdagioBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final AdagioBidder target = new AdagioBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new AdagioBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateExpectedUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://test.endpoint.com"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedBody() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("imp")); + + // when + final Result>> results = target.makeHttpRequests(bidRequest); + + // then + assertThat(results.getValue()).hasSize(1) + .extracting(HttpRequest::getBody, HttpRequest::getPayload) + .containsExactly(tuple(jacksonMapper.encodeToBytes(bidRequest), bidRequest)); + assertThat(results.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenBidResponseOrSeatBidAreNull() throws JsonProcessingException { + // given + final BidderCall invalidHttpCall = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(invalidHttpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1).containsExactly(badServerResponse("empty seatbid array")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenMtypeIs1() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bannerBid, BidType.banner, "adagio", "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidWhenMtypeIs2() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(2, ext -> ext.video(ExtBidPrebidVideo.of(10, "cat"))); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.builder() + .type(BidType.video) + .seat("adagio") + .bidCurrency("USD") + .bid(videoBid) + .videoInfo(ExtBidPrebidVideo.of(10, "cat")) + .build()); + } + + @Test + public void makeBidsShouldReturnNativeBidWhenMtypeIs4() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(nativeBid, BidType.xNative, "adagio", "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeIsMissing() throws JsonProcessingException { + // given + final Bid bidWithMissingMtype = givenBid(null); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bidWithMissingMtype)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(BidderError.badServerResponse("Could not define media type for impression: impId")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMtypeIsUnsupported() throws JsonProcessingException { + // given + final Bid bidWithUnsupportedMtype = givenBid(3); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bidWithUnsupportedMtype)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .containsExactly(BidderError.badServerResponse("Could not define media type for impression: impId")); + } + + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return BidRequest.builder().imp(singletonList(givenImp(impCustomizer))).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static Bid givenBid(Integer mtype) { + return givenBid(mtype, identity()); + } + + private static Bid givenBid(Integer mtype, UnaryOperator extCustomizer) { + final ExtBidPrebid extBidPrebid = extCustomizer.apply(ExtBidPrebid.builder()).build(); + return Bid.builder() + .id("bidId") + .impid("impId") + .price(BigDecimal.ONE) + .mtype(mtype) + .ext(mapper.valueToTree(extBidPrebid)) + .build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().seat("adagio").bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/AdagioTest.java b/src/test/java/org/prebid/server/it/AdagioTest.java new file mode 100644 index 00000000000..01279362d4f --- /dev/null +++ b/src/test/java/org/prebid/server/it/AdagioTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class AdagioTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAdagio() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/adagio-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/adagio/test-adagio-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/adagio/test-adagio-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/adagio/test-auction-adagio-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/adagio/test-auction-adagio-response.json", response, List.of("adagio")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-request.json new file mode 100644 index 00000000000..607314934f3 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-request.json @@ -0,0 +1,55 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "organizationId" : "organizationId", + "placement": "placement" + } + } + } + ], + "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": { + "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/adagio/test-adagio-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-response.json new file mode 100644 index 00000000000..940bae9975c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-adagio-bid-response.json @@ -0,0 +1,21 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid001", + "crid": "crid001", + "cid": "cid001", + "adm": "adm001", + "mtype": 2, + "h": 250, + "w": 300 + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-request.json b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-request.json new file mode 100644 index 00000000000..3a07c1ece20 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-request.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "adagio": { + "organizationId" : "organizationId", + "placement": "placement" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-response.json new file mode 100644 index 00000000000..7361ed22f23 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/adagio/test-auction-adagio-response.json @@ -0,0 +1,43 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 1500, + "price": 3.33, + "adm": "adm001", + "adid": "adid001", + "cid": "cid001", + "crid": "crid001", + "mtype": 2, + "w": 300, + "h": 250, + "ext": { + "origbidcpm": 3.33, + "prebid": { + "type": "video", + "meta": { + "adaptercode": "adagio" + } + } + } + } + ], + "seat": "adagio", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "adagio": "{{ adagio.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index f3c83c787a7..cda8c8f3d4a 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -20,6 +20,8 @@ adapters.aceex.enabled=true adapters.aceex.endpoint=http://localhost:8090/aceex-exchange adapters.acuityads.enabled=true adapters.acuityads.endpoint=http://localhost:8090/acuityads-exchange +adapters.adagio.enabled=true +adapters.adagio.endpoint=http://localhost:8090/adagio-exchange adapters.adelement.enabled=true adapters.adelement.endpoint=http://localhost:8090/adelement-exchange adapters.adf.enabled=true From 6e977ecea03c5e877063f51a3ab14e0589ad52ac Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:33:23 +0200 Subject: [PATCH 46/51] New AdupTech Adapter (#4024) --- .../bidder/aduptech/AduptechBidder.java | 172 +++++++++ .../ext/request/aduptech/ExtImpAduptech.java | 21 ++ .../config/bidder/AduptechConfiguration.java | 60 ++++ .../resources/bidder-config/aduptech.yaml | 25 ++ .../static/bidder-params/aduptech.json | 38 ++ .../bidder/aduptech/AduptechBidderTest.java | 328 ++++++++++++++++++ .../org/prebid/server/it/AduptechTest.java | 34 ++ .../aduptech/test-aduptech-bid-request.json | 55 +++ .../aduptech/test-aduptech-bid-response.json | 19 + .../test-auction-aduptech-request.json | 22 ++ .../test-auction-aduptech-response.json | 40 +++ .../server/it/test-application.properties | 3 + 12 files changed, 817 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java create mode 100644 src/main/resources/bidder-config/aduptech.yaml create mode 100644 src/main/resources/static/bidder-params/aduptech.json create mode 100644 src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AduptechTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json diff --git a/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java new file mode 100644 index 00000000000..d6a826e9977 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java @@ -0,0 +1,172 @@ +package org.prebid.server.bidder.aduptech; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Currency; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class AduptechBidder implements Bidder { + + private static final String COMPONENT_ID_HEADER = "Componentid"; + private static final String COMPONENT_ID_HEADER_VALUE = "prebid-java"; + private static final String DEFAULT_BID_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + private final String targetCurrency; + + public AduptechBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService, + String targetCurrency) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.targetCurrency = validateCurrency(targetCurrency); + } + + private static String validateCurrency(String code) { + try { + Currency.getInstance(code); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid extra info: invalid TargetCurrency %s".formatted(code)); + } + return code.toUpperCase(); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(request.getImp().size()); + for (Imp imp : request.getImp()) { + try { + modifiedImps.add(modifyImp(imp, request)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = request.toBuilder().imp(modifiedImps).build(); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + makeHeaders(), + endpointUrl, + mapper); + + return Result.withValue(httpRequest); + } + + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + Price impFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + impFloorPrice = BidderUtil.isValidPrice(impFloorPrice) + && !targetCurrency.equalsIgnoreCase(impFloorPrice.getCurrency()) + ? convertBidFloor(impFloorPrice, bidRequest) + : impFloorPrice; + + return imp.toBuilder() + .bidfloor(impFloorPrice.getValue()) + .bidfloorcur(impFloorPrice.getCurrency()) + .build(); + } + + private Price convertBidFloor(Price impFloorPrice, BidRequest bidRequest) { + try { + return convertToTargetCurrency(impFloorPrice.getValue(), bidRequest, impFloorPrice.getCurrency()); + } catch (PreBidException e) { + final BigDecimal defaultCurrencyBidFloor = currencyConversionService.convertCurrency( + impFloorPrice.getValue(), + bidRequest, + impFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + return convertToTargetCurrency(defaultCurrencyBidFloor, bidRequest, DEFAULT_BID_CURRENCY); + } + } + + private Price convertToTargetCurrency(BigDecimal impFloorPrice, BidRequest bidRequest, String fromCurrency) { + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + impFloorPrice, + bidRequest, + fromCurrency, + targetCurrency); + + return Price.of(targetCurrency, convertedFloor); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers().add(COMPONENT_ID_HEADER, COMPONENT_ID_HEADER_VALUE); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final List errors = new ArrayList<>(); + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private static List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + try { + return BidderBid.of(bid, getBidType(bid.getMtype()), currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Integer markupType) { + return switch (markupType) { + case 1 -> BidType.banner; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unknown markup type: " + markupType); + }; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java new file mode 100644 index 00000000000..6e92ad45db7 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java @@ -0,0 +1,21 @@ +package org.prebid.server.proto.openrtb.ext.request.aduptech; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAduptech { + + String publisher; + + String placement; + + String query; + + Boolean adtest; + + Boolean debug; + + ObjectNode ext; +} + diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java new file mode 100644 index 00000000000..5011fc46c41 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java @@ -0,0 +1,60 @@ +package org.prebid.server.spring.config.bidder; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.aduptech.AduptechBidder; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/aduptech.yaml", factory = YamlPropertySourceFactory.class) +public class AduptechConfiguration { + + private static final String BIDDER_NAME = "aduptech"; + + @Bean("aduptechConfigurationProperties") + @ConfigurationProperties("adapters.aduptech") + AduptechConfigurationProperties configurationProperties() { + return new AduptechConfigurationProperties(); + } + + @Bean + BidderDeps aduptechBidderDeps(AduptechConfigurationProperties aduptechConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(aduptechConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AduptechBidder( + config.getEndpoint(), + mapper, + currencyConversionService, + config.getTargetCurrency())) + .assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class AduptechConfigurationProperties extends BidderConfigurationProperties { + + @NotNull + private String targetCurrency; + } +} diff --git a/src/main/resources/bidder-config/aduptech.yaml b/src/main/resources/bidder-config/aduptech.yaml new file mode 100644 index 00000000000..302051085bd --- /dev/null +++ b/src/main/resources/bidder-config/aduptech.yaml @@ -0,0 +1,25 @@ +adapters: + aduptech: + endpoint: https://rtb.d.adup-tech.com/rtb/bid + ortb-version: "2.6" + meta-info: + maintainer-email: support@adup-tech.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 647 + usersync: + cookie-family-name: aduptech + iframe: + url: https://rtb.d.adup-tech.com/service/sync?iframe=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + redirect: + url: https://rtb.d.adup-tech.com/service/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + target-currency: "EUR" diff --git a/src/main/resources/static/bidder-params/aduptech.json b/src/main/resources/static/bidder-params/aduptech.json new file mode 100644 index 00000000000..b2d7a8817ea --- /dev/null +++ b/src/main/resources/static/bidder-params/aduptech.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdUp Tech adapter params", + "description": "A schema which validates params accepted by the AdUp Tech adapter", + "type": "object", + "properties": { + "publisher": { + "type": "string", + "minLength": 1, + "description": "Unique publisher identifier." + }, + "placement": { + "type": "string", + "minLength": 1, + "description": "Unique placement identifier per publisher." + }, + "query": { + "type": "string", + "description": "Semicolon separated list of keywords." + }, + "adtest": { + "type": "boolean", + "description": "Deactivates tracking of impressions and clicks. **Should only be used for testing purposes!**" + }, + "debug": { + "type": "boolean", + "description": "Enables debug mode. **Should only be used for testing purposes!**" + }, + "ext": { + "type": "object", + "description": "Additional parameters to be included in the request." + } + }, + "required": [ + "publisher", + "placement" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java b/src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java new file mode 100644 index 00000000000..8b52853beba --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/aduptech/AduptechBidderTest.java @@ -0,0 +1,328 @@ +package org.prebid.server.bidder.aduptech; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.bidder.model.BidderError.badInput; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class AduptechBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + private static final String TARGET_CURRENCY = "EUR"; + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyConversionService; + + private AduptechBidder target; + + @BeforeEach + public void setUp() { + target = new AduptechBidder(ENDPOINT_URL, jacksonMapper, currencyConversionService, TARGET_CURRENCY); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AduptechBidder("invalid_url", + jacksonMapper, + currencyConversionService, + TARGET_CURRENCY)); + } + + @Test + public void creationShouldFailOnInvalidTargetCurrency() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AduptechBidder(ENDPOINT_URL, + jacksonMapper, + currencyConversionService, + "invalid_currency")) + .withMessage("invalid extra info: invalid TargetCurrency invalid_currency"); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfCurrencyConversionFails() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + given(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .willThrow(new PreBidException("test-error")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).containsExactly(badInput("test-error")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldConvertBidFloorIfCurrencyIsDifferent() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("USD")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "USD", TARGET_CURRENCY)) + .willReturn(BigDecimal.valueOf(12.0)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.valueOf(12.0), TARGET_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldPerformTwoStepCurrencyConversionIfInitialConversionFails() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.bidfloor(BigDecimal.TEN).bidfloorcur("CAD")); + + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "CAD", TARGET_CURRENCY)) + .willThrow(new PreBidException("initial conversion failed")); + given(currencyConversionService.convertCurrency(BigDecimal.TEN, bidRequest, "CAD", "USD")) + .willReturn(BigDecimal.valueOf(8)); + given(currencyConversionService.convertCurrency(BigDecimal.valueOf(8), bidRequest, "USD", TARGET_CURRENCY)) + .willReturn(BigDecimal.valueOf(9.6)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsExactly(tuple(BigDecimal.valueOf(9.6), TARGET_CURRENCY)); + } + + @Test + public void makeHttpRequestsShouldNotModifyImpIfBidFloorIsNotValid() { + // given + final Imp imp = givenImp(builder -> builder.bidfloor(BigDecimal.ZERO).bidfloorcur("USD")); + final BidRequest bidRequest = givenBidRequest(imp); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactly(imp); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldNotModifyImpIfBidFloorCurrencyIsSameAsTarget() { + // given + final Imp imp = givenImp(builder -> builder.bidfloor(BigDecimal.TEN).bidfloorcur(TARGET_CURRENCY)); + final BidRequest bidRequest = givenBidRequest(imp); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .containsExactly(imp); + + verifyNoInteractions(currencyConversionService); + } + + @Test + public void makeHttpRequestsShouldCreateSingleRequestWithAllImps() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1"), + imp -> imp.id("imp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getImpIds) + .containsExactly(Set.of("imp1", "imp2")); + } + + @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("Componentid")) + .isEqualTo("prebid-java")) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedEndpointUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getUri) + .isEqualTo(ENDPOINT_URL); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidSuccessfully() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(givenBid(2))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("Unknown markup type: 2")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(List.of(imps)).build(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(AduptechBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static Bid givenBid(Integer mtype) { + return Bid.builder().mtype(mtype).build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/AduptechTest.java b/src/test/java/org/prebid/server/it/AduptechTest.java new file mode 100644 index 00000000000..7db6a6c1217 --- /dev/null +++ b/src/test/java/org/prebid/server/it/AduptechTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +public class AduptechTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAduptech() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/aduptech-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/aduptech/test-aduptech-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/aduptech/test-aduptech-bid-response.json")))); + + // when + final Response response = responseFor( + "openrtb2/aduptech/test-auction-aduptech-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/aduptech/test-auction-aduptech-response.json", response, List.of("aduptech")); + } + +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json new file mode 100644 index 00000000000..a426963a557 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-request.json @@ -0,0 +1,55 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "placement": "placement", + "publisher": "publisher" + } + } + } + ], + "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": { + "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/aduptech/test-aduptech-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json new file mode 100644 index 00000000000..939afc367be --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-aduptech-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 0.01, + "id": "bid_id", + "impid": "imp_id", + "mtype": 1, + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json new file mode 100644 index 00000000000..3c1b7c64524 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-request.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "ext": { + "aduptech": { + "placement": "placement", + "publisher": "publisher" + } + } + } + ], + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json new file mode 100644 index 00000000000..f7a658d8047 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aduptech/test-auction-aduptech-response.json @@ -0,0 +1,40 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 0.01, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "aduptech" + } + }, + "origbidcpm": 0.01 + } + } + ], + "seat": "aduptech", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "aduptech": "{{ aduptech.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index cda8c8f3d4a..1c5761b441a 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -95,6 +95,9 @@ adapters.adtonos.enabled=true adapters.adtonos.endpoint=http://localhost:8090/adtonos-exchange/{{PublisherId}} adapters.adtrgtme.enabled=true adapters.adtrgtme.endpoint=http://localhost:8090/adtrgtme-exchange +adapters.aduptech.enabled=true +adapters.aduptech.endpoint=http://localhost:8090/aduptech-exchange +adapters.aduptech.target-currency=EUR adapters.advangelists.enabled=true adapters.advangelists.endpoint=http://localhost:8090/advangelists-exchange?pubid={{PublisherID}} adapters.adxcg.enabled=true From f26254f2e9ab55528a54b94d55109e112ea834fa Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:45:52 +0200 Subject: [PATCH 47/51] New Smoot Adapter (#4021) --- .../server/bidder/smoot/SmootBidder.java | 117 ++++++++ .../server/bidder/smoot/SmootImpExt.java | 24 ++ .../ext/request/smoot/ExtImpSmoot.java | 14 + .../config/bidder/SmootConfiguration.java | 41 +++ src/main/resources/bidder-config/smoot.yaml | 21 ++ .../resources/static/bidder-params/smoot.json | 30 ++ .../server/bidder/smoot/SmootBidderTest.java | 282 ++++++++++++++++++ .../java/org/prebid/server/it/SmootTest.java | 34 +++ .../smoot/test-auction-smoot-request.json | 23 ++ .../smoot/test-auction-smoot-response.json | 43 +++ .../smoot/test-smoot-bid-request.json | 56 ++++ .../smoot/test-smoot-bid-response.json | 26 ++ .../server/it/test-application.properties | 2 + 13 files changed, 713 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/smoot/SmootBidder.java create mode 100644 src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java create mode 100644 src/main/resources/bidder-config/smoot.yaml create mode 100644 src/main/resources/static/bidder-params/smoot.json create mode 100644 src/test/java/org/prebid/server/bidder/smoot/SmootBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/SmootTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/smoot/SmootBidder.java b/src/main/java/org/prebid/server/bidder/smoot/SmootBidder.java new file mode 100644 index 00000000000..ec054d9982d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smoot/SmootBidder.java @@ -0,0 +1,117 @@ +package org.prebid.server.bidder.smoot; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.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.smoot.ExtImpSmoot; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class SmootBidder implements Bidder { + + private static final TypeReference> SMOOT_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SmootBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpSmoot extImpSmoot = parseImpExt(imp); + final Imp modifiedImp = modifyImp(imp, extImpSmoot); + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(modifiedImp)) + .build(); + httpRequests.add(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpSmoot parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), SMOOT_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext: " + e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpSmoot extImpSmoot) { + final SmootImpExt smootImpExt = StringUtils.isNotEmpty(extImpSmoot.getPlacementId()) + ? SmootImpExt.publisher(extImpSmoot.getPlacementId()) + : SmootImpExt.network(extImpSmoot.getEndpointId()); + + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set("bidder", mapper.mapper().valueToTree(smootImpExt))) + .build(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidResponse)); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> + throw new PreBidException("could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java b/src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java new file mode 100644 index 00000000000..214b6f37bc9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java @@ -0,0 +1,24 @@ +package org.prebid.server.bidder.smoot; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SmootImpExt { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; + + public static SmootImpExt publisher(String placementId) { + return SmootImpExt.of("publisher", placementId, null); + } + + public static SmootImpExt network(String endpointId) { + return SmootImpExt.of("network", null, endpointId); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java new file mode 100644 index 00000000000..6f7e8775a91 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.smoot; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSmoot { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java new file mode 100644 index 00000000000..154bb8c4779 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.smoot.SmootBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/smoot.yaml", factory = YamlPropertySourceFactory.class) +public class SmootConfiguration { + + private static final String BIDDER_NAME = "smoot"; + + @Bean("smootConfigurationProperties") + @ConfigurationProperties("adapters.smoot") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps smootBidderDeps(BidderConfigurationProperties smootConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(smootConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SmootBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/smoot.yaml b/src/main/resources/bidder-config/smoot.yaml new file mode 100644 index 00000000000..a1690cbb8ba --- /dev/null +++ b/src/main/resources/bidder-config/smoot.yaml @@ -0,0 +1,21 @@ +adapters: + smoot: + endpoint: 'https://endpoint1.smoot.ai/pserver' + meta-info: + maintainer-email: 'info@smoot.ai' + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: smoot + redirect: + support-cors: false + url: 'https://usync.smxconv.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}}' + uid-macro: '[UID]' diff --git a/src/main/resources/static/bidder-params/smoot.json b/src/main/resources/static/bidder-params/smoot.json new file mode 100644 index 00000000000..017047107cf --- /dev/null +++ b/src/main/resources/static/bidder-params/smoot.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smoot Adapter Params", + "description": "A schema which validates params accepted by the Smoot adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "endpointId": { + "type": "string", + "minLength": 1, + "description": "Endpoint ID" + } + }, + "oneOf": [ + { + "required": [ + "placementId" + ] + }, + { + "required": [ + "endpointId" + ] + } + ] +} diff --git a/src/test/java/org/prebid/server/bidder/smoot/SmootBidderTest.java b/src/test/java/org/prebid/server/bidder/smoot/SmootBidderTest.java new file mode 100644 index 00000000000..edaeb6fb695 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/smoot/SmootBidderTest.java @@ -0,0 +1,282 @@ +package org.prebid.server.bidder.smoot; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.smoot.ExtImpSmoot; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +@ExtendWith(MockitoExtension.class) +public class SmootBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.endpoint.com"; + + private final SmootBidder target = new SmootBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new SmootBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(mapper.valueToTree( + ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Error parsing imp.ext:"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateSeparateRequestForEachImp() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1").ext(givenPlacementImpExt("placement")), + imp -> imp.id("imp2").ext(givenEndpointImpExt("endpoint"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactly(Set.of("imp1"), Set.of("imp2")); + } + + @Test + public void makeHttpRequestsShouldSetCorrectUriAndHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenPlacementImpExt("placement"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + } + + @Test + public void makeHttpRequestsShouldModifyImpExtForPublisherType() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenPlacementImpExt("placement"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedExtNode = mapper.createObjectNode().set("bidder", mapper.createObjectNode() + .put("type", "publisher") + .put("placementId", "placement")); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedExtNode); + } + + @Test + public void makeHttpRequestsShouldModifyImpExtForNetworkType() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenEndpointImpExt("endpoint"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + final ObjectNode expectedExtNode = mapper.createObjectNode().set("bidder", mapper.createObjectNode() + .put("type", "network") + .put("endpointId", "endpoint")); + + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(expectedExtNode); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListWhenBidResponseIsNull() throws JsonProcessingException { + // given + final BidderCall noSeatBids = givenHttpCall(mapper.writeValueAsString(null)); + + // when + final Result> result = target.makeBids(noSeatBids, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidWhenMtypeIsBanner() throws JsonProcessingException { + // given + final Bid bannerBid = givenBid(1); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bannerBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidWhenMtypeIsVideo() throws JsonProcessingException { + // given + final Bid videoBid = givenBid(2); + final BidderCall httpCall = givenHttpCall(givenBidResponse(videoBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidWhenMtypeIsNative() throws JsonProcessingException { + // given + final Bid nativeBid = givenBid(4); + final BidderCall httpCall = givenHttpCall(givenBidResponse(nativeBid)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).containsExactly(BidderBid.of(nativeBid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsMissing() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(givenBid(null))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("could not define media type for impression: impId")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfMtypeIsUnsupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(givenBidResponse(givenBid(3))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse("could not define media type for impression: impId")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(SmootBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("impId")).build(); + } + + private static ObjectNode givenPlacementImpExt(String placementId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpSmoot.of(placementId, null))); + } + + private static ObjectNode givenEndpointImpExt(String endpointId) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpSmoot.of(null, endpointId))); + } + + private static Bid givenBid(Integer mtype) { + return Bid.builder().id("bidId").impid("impId").price(BigDecimal.ONE).mtype(mtype).build(); + } + + private static String givenBidResponse(Bid bid) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(singletonList(bid)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/SmootTest.java b/src/test/java/org/prebid/server/it/SmootTest.java new file mode 100644 index 00000000000..b6c2545d4a5 --- /dev/null +++ b/src/test/java/org/prebid/server/it/SmootTest.java @@ -0,0 +1,34 @@ +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 SmootTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromSmoot() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/smoot-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/smoot/test-smoot-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom( + "openrtb2/smoot/test-smoot-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/smoot/test-auction-smoot-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/smoot/test-auction-smoot-response.json", response, + singletonList("smoot")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-request.json new file mode 100644 index 00000000000..e614ff78baf --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "smoot": { + "placementId": "placementId" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-response.json new file mode 100644 index 00000000000..60df35732e0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-auction-smoot-response.json @@ -0,0 +1,43 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 3.33, + "adm": "adm001", + "adid": "adid", + "cid": "cid", + "crid": "crid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "smoot" + } + }, + "origbidcpm": 3.33 + } + } + ], + "seat": "smoot", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smoot": "{{ smoot.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-request.json new file mode 100644 index 00000000000..ae0589ead56 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "placementId": "placementId", + "type": "publisher" + } + } + } + ], + "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/smoot/test-smoot-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-response.json new file mode 100644 index 00000000000..78648154f4e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/smoot/test-smoot-bid-response.json @@ -0,0 +1,26 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 3.33, + "adid": "adid", + "crid": "crid", + "cid": "cid", + "adm": "adm001", + "mtype": 1, + "h": 250, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 1c5761b441a..6611c1648e4 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -489,6 +489,8 @@ adapters.smartyads.enabled=true adapters.smartyads.endpoint=http://localhost:8090/smartyads-exchange adapters.smilewanted.enabled=true adapters.smilewanted.endpoint=http://localhost:8090/smilewanted-exchange +adapters.smoot.enabled=true +adapters.smoot.endpoint=http://localhost:8090/smoot-exchange adapters.smrtconnect.enabled=true adapters.smrtconnect.endpoint=http://localhost:8090/smrtconnect-exchange?supply_id={{SupplyId}} adapters.sonobi.enabled=true From 35b656225d32c7a3a98e466bae73bce98308ecd5 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:00:40 +0200 Subject: [PATCH 48/51] New BidTheatre Adapter (#4023) --- .../bidder/bidtheatre/BidTheatreBidder.java | 118 +++++++++ .../request/bidtheatre/ExtImpBidTheatre.java | 12 + .../bidder/BidTheatreConfiguration.java | 41 +++ .../resources/bidder-config/bidtheatre.yaml | 14 ++ .../static/bidder-params/bidtheatre.json | 16 ++ .../bidtheatre/BidTheatreBidderTest.java | 238 ++++++++++++++++++ .../org/prebid/server/it/BidTheatreTest.java | 37 +++ .../test-auction-bidtheatre-request.json | 23 ++ .../test-auction-bidtheatre-response.json | 39 +++ .../test-bidtheatre-bid-request.json | 56 +++++ .../test-bidtheatre-bid-response.json | 22 ++ .../server/it/test-application.properties | 2 + 12 files changed, 618 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/BidTheatreConfiguration.java create mode 100644 src/main/resources/bidder-config/bidtheatre.yaml create mode 100644 src/main/resources/static/bidder-params/bidtheatre.json create mode 100644 src/test/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/BidTheatreTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidder.java b/src/main/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidder.java new file mode 100644 index 00000000000..1e1fc625175 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidder.java @@ -0,0 +1,118 @@ +package org.prebid.server.bidder.bidtheatre; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.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.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class BidTheatreBidder implements Bidder { + + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BidTheatreBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + return Result.withValue(BidderUtil.defaultRequest(request, endpointUrl, mapper)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidResponse, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + if (bidResponse == null || bidResponse.getSeatbid() == null) { + return Collections.emptyList(); + } + return bidsFromResponse(bidResponse, errors); + } + + private List bidsFromResponse(BidResponse bidResponse, List errors) { + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType mediaType = getMediaType(bid); + if (mediaType == null) { + errors.add(BidderError.badServerResponse("Failed to parse impression \"%s\" mediatype" + .formatted(bid.getImpid()))); + return null; + } + + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; + + final Bid modifiedBid = bid.toBuilder() + .nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString)) + .build(); + + return BidderBid.of(modifiedBid, mediaType, currency); + } + + private BidType getMediaType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(this::parseBidExt) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getType) + .orElse(null); + } + + private ExtPrebid parseBidExt(ObjectNode ext) { + try { + return mapper.mapper().convertValue(ext, EXT_PREBID_TYPE_REFERENCE); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java new file mode 100644 index 00000000000..3eb1696f672 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.bidtheatre; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBidTheatre { + + @JsonProperty("publisherId") + String publisherId; + +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/BidTheatreConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidTheatreConfiguration.java new file mode 100644 index 00000000000..2cbf98062df --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidTheatreConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bidtheatre.BidTheatreBidder; +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/bidtheatre.yaml", factory = YamlPropertySourceFactory.class) +public class BidTheatreConfiguration { + + private static final String BIDDER_NAME = "bidtheatre"; + + @Bean("bidtheatreConfigurationProperties") + @ConfigurationProperties("adapters.bidtheatre") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bidtheatreBidderDeps(BidderConfigurationProperties bidtheatreConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bidtheatreConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BidTheatreBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/bidtheatre.yaml b/src/main/resources/bidder-config/bidtheatre.yaml new file mode 100644 index 00000000000..903778e884a --- /dev/null +++ b/src/main/resources/bidder-config/bidtheatre.yaml @@ -0,0 +1,14 @@ +adapters: + bidtheatre: + endpoint: https://prebidjs-bids.bidtheatre.net/prebidjsbid + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: operations@bidtheatre.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 30 diff --git a/src/main/resources/static/bidder-params/bidtheatre.json b/src/main/resources/static/bidder-params/bidtheatre.json new file mode 100644 index 00000000000..f56899b7ccd --- /dev/null +++ b/src/main/resources/static/bidder-params/bidtheatre.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidtheatre Adapter Params", + "description": "A schema which validates params accepted by the Bidtheatre adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "Publisher ID", + "format": "uuid" + } + }, + "required": [ + "publisherId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidderTest.java b/src/test/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidderTest.java new file mode 100644 index 00000000000..a2ac4062266 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidderTest.java @@ -0,0 +1,238 @@ +package org.prebid.server.bidder.bidtheatre; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderCall; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.HttpResponse; +import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.bidtheatre.ExtImpBidTheatre; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.prebid.server.bidder.model.BidderError.Type.bad_server_response; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class BidTheatreBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://randomurl.com"; + + private final BidTheatreBidder target = new BidTheatreBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void shouldFailOnBidderCreation() { + assertThatIllegalArgumentException().isThrownBy(() -> new BidTheatreBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldCreateExpectedUrl() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://randomurl.com"); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER)) + .isEqualTo(APPLICATION_JSON_CONTENT_TYPE)) + .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER)) + .isEqualTo(APPLICATION_JSON_VALUE)); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldReturnExpectedBody() { + // given + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("imp1"), imp -> imp.id("imp2")); + + // when + final Result>> results = target.makeHttpRequests(bidRequest); + + // then + assertThat(results.getValue()).hasSize(1) + .extracting(HttpRequest::getBody, HttpRequest::getPayload) + .containsExactly(tuple(jacksonMapper.encodeToBytes(bidRequest), bidRequest)); + assertThat(results.getErrors()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("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(bad_server_response); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListWhenBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall(); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorAndEmptyListWhenBidTypeCanBeResolved() throws JsonProcessingException { + // given + final Bid invalidBid1 = Bid.builder().impid("imp_id1").ext(mapper.createObjectNode().put("prebid", 2)).build(); + final Bid invalidBid2 = Bid.builder().impid("imp_id2").ext(mapper.createObjectNode()).build(); + final BidderCall httpCall = givenHttpCall(invalidBid1, invalidBid2); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(2) + .extracting(BidderError::getMessage, BidderError::getType) + .containsExactly( + tuple("Failed to parse impression \"imp_id1\" mediatype", bad_server_response), + tuple("Failed to parse impression \"imp_id2\" mediatype", bad_server_response)); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBidWithResolvedMacros() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.valueToTree(ExtPrebid.of( + ExtBidPrebid.builder().type(BidType.banner).build(), null)); + final Bid givenBid = Bid.builder() + .impid("imp_id") + .price(BigDecimal.valueOf(3.32)) + .nurl("nurl_${AUCTION_PRICE}_nurl") + .adm("adm_${AUCTION_PRICE}_adm") + .ext(bidExt) + .build(); + final BidderCall httpCall = givenHttpCall(givenBid); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .satisfies(bid -> { + assertThat(bid.getBidCurrency()).isEqualTo("USD"); + assertThat(bid.getType()).isEqualTo(BidType.banner); + assertThat(bid.getBid().getAdm()).isEqualTo("adm_3.32_adm"); + assertThat(bid.getBid().getNurl()).isEqualTo("nurl_3.32_nurl"); + assertThat(bid.getBid().getPrice()).isEqualTo(BigDecimal.valueOf(3.32)); + }); + } + + @Test + public void makeBidsShouldReturnBidWithResolvedMacrosWhenPriceIsEmpty() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.valueToTree(ExtPrebid.of( + ExtBidPrebid.builder().type(BidType.banner).build(), null)); + final Bid givenBid = Bid.builder() + .impid("imp_id") + .price(null) + .nurl("nurl_${AUCTION_PRICE}_nurl") + .adm("adm_${AUCTION_PRICE}_adm") + .ext(bidExt) + .build(); + final BidderCall httpCall = givenHttpCall(givenBid); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .satisfies(bid -> { + assertThat(bid.getBidCurrency()).isEqualTo("USD"); + assertThat(bid.getType()).isEqualTo(BidType.banner); + assertThat(bid.getBid().getAdm()).isEqualTo("adm_0_adm"); + assertThat(bid.getBid().getNurl()).isEqualTo("nurl_0_nurl"); + assertThat(bid.getBid().getPrice()).isNull(); + }); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + return BidRequest.builder() + .imp(Arrays.stream(impCustomizers).map(BidTheatreBidderTest::givenImp).toList()) + .build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder().id("imp_id").ext(givenImpExt())).build(); + } + + private static ObjectNode givenImpExt() { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpBidTheatre.of("publisherId"))); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(Bid... bids) throws JsonProcessingException { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, givenBidResponse(bids)), + null); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().payload(null).build(), + HttpResponse.of(200, null, body), + null); + } + +} diff --git a/src/test/java/org/prebid/server/it/BidTheatreTest.java b/src/test/java/org/prebid/server/it/BidTheatreTest.java new file mode 100644 index 00000000000..419dcf71cda --- /dev/null +++ b/src/test/java/org/prebid/server/it/BidTheatreTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.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 BidTheatreTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromAso() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/bidtheatre-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/bidtheatre/test-bidtheatre-bid-request.json"))) + .willReturn(aResponse().withBody( + jsonFrom("openrtb2/bidtheatre/test-bidtheatre-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/bidtheatre/test-auction-bidtheatre-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals( + "openrtb2/bidtheatre/test-auction-bidtheatre-response.json", + response, + singletonList("bidtheatre")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-request.json new file mode 100644 index 00000000000..fc53d5f98b9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-request.json @@ -0,0 +1,23 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidtheatre": { + "publisherId" : "73b20b3a-12a0-4869-b54e-8d42b55786ee" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-response.json new file mode 100644 index 00000000000..ebfbdc67de5 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-auction-bidtheatre-response.json @@ -0,0 +1,39 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "exp": 300, + "price": 4.7, + "adm": "adm6_4.7", + "nurl": "nurl_4.7", + "crid": "crid6", + "ext": { + "prebid": { + "type": "banner", + "meta": { + "adaptercode": "bidtheatre" + } + }, + "origbidcpm": 4.7 + } + } + ], + "seat": "bidtheatre", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "bidtheatre": "{{ bidtheatre.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-request.json new file mode 100644 index 00000000000..0b80c8070f0 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-request.json @@ -0,0 +1,56 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "secure": 1, + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder" : { + "publisherId" : "73b20b3a-12a0-4869-b54e-8d42b55786ee" + } + } + } + ], + "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/bidtheatre/test-bidtheatre-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-response.json new file mode 100644 index 00000000000..665be9472f4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/bidtheatre/test-bidtheatre-bid-response.json @@ -0,0 +1,22 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 4.7, + "adm": "adm6_${AUCTION_PRICE}", + "nurl": "nurl_${AUCTION_PRICE}", + "crid": "crid6", + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index 6611c1648e4..d5b362a6f9b 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -157,6 +157,8 @@ adapters.bidscube.enabled=true adapters.bidscube.endpoint=http://localhost:8090/bidscube-exchange adapters.bidstack.enabled=true adapters.bidstack.endpoint=http://localhost:8090/bidstack-exchange +adapters.bidtheatre.enabled=true +adapters.bidtheatre.endpoint=http://localhost:8090/bidtheatre-exchange adapters.bigoad.enabled=true adapters.bigoad.endpoint=http://localhost:8090/bigoad-exchange adapters.blasto.enabled=true From a9d01440d1dc5ca68c7559cc8d02c0b8f4d607c9 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:00:51 +0200 Subject: [PATCH 49/51] New Flatads Adapter (#4045) --- .../server/bidder/flatads/FlatadsBidder.java | 164 ++++++++++++ .../ext/request/flatads/ExtImpFlatads.java | 13 + .../config/bidder/FlatadsConfiguration.java | 41 +++ src/main/resources/bidder-config/flatads.yaml | 17 ++ .../static/bidder-params/flatads.json | 22 ++ .../bidder/flatads/FlatadsBidderTest.java | 250 ++++++++++++++++++ .../org/prebid/server/it/FlatadsTest.java | 32 +++ .../flatads/test-auction-flatads-request.json | 38 +++ .../test-auction-flatads-response.json | 44 +++ .../flatads/test-flatads-bid-request.json | 65 +++++ .../flatads/test-flatads-bid-response.json | 27 ++ .../server/it/test-application.properties | 2 + 12 files changed, 715 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/flatads/FlatadsBidder.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java create mode 100644 src/main/resources/bidder-config/flatads.yaml create mode 100644 src/main/resources/static/bidder-params/flatads.json create mode 100644 src/test/java/org/prebid/server/bidder/flatads/FlatadsBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/FlatadsTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-response.json diff --git a/src/main/java/org/prebid/server/bidder/flatads/FlatadsBidder.java b/src/main/java/org/prebid/server/bidder/flatads/FlatadsBidder.java new file mode 100644 index 00000000000..11af869d1da --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/flatads/FlatadsBidder.java @@ -0,0 +1,164 @@ +package org.prebid.server.bidder.flatads; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.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.flatads.ExtImpFlatads; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class FlatadsBidder implements Bidder { + + private static final TypeReference> FLATADS_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String TOKEN_ID_MACRO = "{{TokenID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public FlatadsBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpFlatads extImpFlatads = parseImpExt(imp); + final String resolvedEndpoint = resolveEndpoint(extImpFlatads); + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + httpRequests.add(makeHttpRequest(outgoingRequest, resolvedEndpoint)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpFlatads parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), FLATADS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize Flatads extension: " + e.getMessage()); + } + } + + private String resolveEndpoint(ExtImpFlatads extImp) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getPublisherId()))) + .replace(TOKEN_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getToken()))); + } + + private HttpRequest makeHttpRequest(BidRequest request, String endpoint) { + return BidderUtil.defaultRequest(request, makeHeaders(request.getDevice()), endpoint, mapper); + } + + private static MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + return headers; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + final List errors = new ArrayList<>(); + return Result.of(extractBids(bidRequest, bidResponse, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { + + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, bidResponse, errors); + } + + private static List bidsFromResponse(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { + + final Map imps = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, imps, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(Bid bid, Map imps, String currency, List errors) { + final BidType bidType = getBidType(bid.getImpid(), imps, errors); + return bidType == null ? null : BidderBid.of(bid, bidType, currency); + } + + private static BidType getBidType(String impId, Map imps, List errors) { + final Imp imp = imps.get(impId); + if (imp != null) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + } + + errors.add(BidderError.badServerResponse("The impression with ID %s is not present into the request" + .formatted(impId))); + return null; + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java new file mode 100644 index 00000000000..304cd887ebb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.flatads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpFlatads { + + String token; + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java new file mode 100644 index 00000000000..eef57c1c0a9 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.flatads.FlatadsBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; +import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/flatads.yaml", factory = YamlPropertySourceFactory.class) +public class FlatadsConfiguration { + + private static final String BIDDER_NAME = "flatads"; + + @Bean("flatadsConfigurationProperties") + @ConfigurationProperties("adapters.flatads") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps flatadsBidderDeps(BidderConfigurationProperties flatadsConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(flatadsConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new FlatadsBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/resources/bidder-config/flatads.yaml b/src/main/resources/bidder-config/flatads.yaml new file mode 100644 index 00000000000..970d1c36b79 --- /dev/null +++ b/src/main/resources/bidder-config/flatads.yaml @@ -0,0 +1,17 @@ +adapters: + flatads: + endpoint: https://bid.rtbshark.com/api/rtbs/adx/rtb?x-net-id={{PublisherID}}&x-net-token={{TokenID}} + ortb-version: "2.6" + endpoint-compression: gzip + meta-info: + maintainer-email: adxbusiness@flat-ads.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/flatads.json b/src/main/resources/static/bidder-params/flatads.json new file mode 100644 index 00000000000..9e36aa2c65c --- /dev/null +++ b/src/main/resources/static/bidder-params/flatads.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Flatads Adapter Params", + "description": "A schema which validates params accepted by the Flatads adapter", + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Token of the publisher", + "minLength": 1 + }, + "publisherId": { + "type": "string", + "description": "Flatads Publisher Id", + "minLength": 1 + } + }, + "required": [ + "token", + "publisherId" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/flatads/FlatadsBidderTest.java b/src/test/java/org/prebid/server/bidder/flatads/FlatadsBidderTest.java new file mode 100644 index 00000000000..d8e9e6fe27e --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/flatads/FlatadsBidderTest.java @@ -0,0 +1,250 @@ +package org.prebid.server.bidder.flatads; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +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.flatads.ExtImpFlatads; +import org.prebid.server.proto.openrtb.ext.response.BidType; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.prebid.server.bidder.model.BidderError.badServerResponse; +import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER; +import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE; +import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER; +import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER; +import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +public class FlatadsBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://endpoint.com/?publisher={{PublisherID}}&token={{TokenID}}"; + + private final FlatadsBidder target = new FlatadsBidder(ENDPOINT_URL, jacksonMapper); + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException().isThrownBy(() -> new FlatadsBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).startsWith("Failed to deserialize Flatads extension:"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeHttpRequestsShouldCreateSeparateRequestForEachImp() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.id("imp1"), + imp -> imp.id("imp2")); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(2) + .extracting(HttpRequest::getImpIds) + .containsExactly(Set.of("imp1"), Set.of("imp2")); + } + + @Test + public void makeHttpRequestsShouldResolveEndpointMacrosCorrectly() { + // given + final BidRequest bidRequest = givenBidRequest( + imp -> imp.ext(givenImpExt("testPublisher", "testToken"))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://endpoint.com/?publisher=testPublisher&token=testToken"); + } + + @Test + public void makeHttpRequestsShouldSetCorrectHeaders() { + // given + final BidRequest bidRequest = givenBidRequest(identity()) + .toBuilder() + .device(Device.builder() + .ua("ua") + .ip("ip") + .ipv6("ipv6") + .build()) + .build(); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).first() + .extracting(HttpRequest::getHeaders) + .satisfies(headers -> { + assertThat(headers.get(CONTENT_TYPE_HEADER)).isEqualTo(APPLICATION_JSON_CONTENT_TYPE); + assertThat(headers.get(ACCEPT_HEADER)).isEqualTo(APPLICATION_JSON_VALUE); + assertThat(headers.get(USER_AGENT_HEADER)).isEqualTo("ua"); + assertThat(headers.getAll(X_FORWARDED_FOR_HEADER)).containsExactly("ip", "ipv6"); + }); + } + + @Test + public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { + // given + final BidderCall httpCall = givenHttpCall("invalid_json"); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response); + assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid_json'"); + }); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException { + // given + final Bid bid = givenBid("impId"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId").banner(Banner.builder().build())); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bid, BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException { + // given + final Bid bid = givenBid("impId"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId").video(Video.builder().build())); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bid, BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnNativeBidSuccessfully() throws JsonProcessingException { + // given + final Bid bid = givenBid("impId"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("impId").xNative(Native.builder().build())); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .containsExactly(BidderBid.of(bid, BidType.xNative, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenImpIdIsMissingInRequest() throws JsonProcessingException { + // given + final Bid bid = givenBid("unknown_imp_id"); + final BidRequest bidRequest = givenBidRequest(imp -> imp.id("known_imp_id")); + final BidderCall httpCall = givenHttpCall(givenBidResponse(bid)); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsExactly(badServerResponse( + "The impression with ID unknown_imp_id is not present into the request")); + assertThat(result.getValue()).isEmpty(); + } + + private static BidRequest givenBidRequest(UnaryOperator... impCustomizers) { + final List imps = Arrays.stream(impCustomizers) + .map(FlatadsBidderTest::givenImp) + .toList(); + return BidRequest.builder().imp(imps).build(); + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder() + .id("impId") + .ext(givenImpExt("publisherId", "token"))) + .build(); + } + + private static ObjectNode givenImpExt(String publisherId, String token) { + return mapper.valueToTree(ExtPrebid.of(null, ExtImpFlatads.of(token, publisherId))); + } + + private static Bid givenBid(String impId) { + return Bid.builder().impid(impId).price(BigDecimal.ONE).build(); + } + + private static String givenBidResponse(Bid... bids) throws JsonProcessingException { + return mapper.writeValueAsString(BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder().bid(List.of(bids)).build())) + .build()); + } + + private static BidderCall givenHttpCall(String body) { + return BidderCall.succeededHttp( + HttpRequest.builder().build(), + HttpResponse.of(200, null, body), + null); + } +} diff --git a/src/test/java/org/prebid/server/it/FlatadsTest.java b/src/test/java/org/prebid/server/it/FlatadsTest.java new file mode 100644 index 00000000000..f2abb839987 --- /dev/null +++ b/src/test/java/org/prebid/server/it/FlatadsTest.java @@ -0,0 +1,32 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.client.WireMock; +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.prebid.server.model.Endpoint; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static java.util.Collections.singletonList; + +public class FlatadsTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromFlatadsBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(WireMock.post(WireMock.urlPathEqualTo("/flatads-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/flatads/test-flatads-bid-request.json"))) + .willReturn(WireMock.aResponse().withBody( + jsonFrom("openrtb2/flatads/test-flatads-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/flatads/test-auction-flatads-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/flatads/test-auction-flatads-response.json", response, + singletonList("flatads")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-request.json b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-request.json new file mode 100644 index 00000000000..d56a393e83a --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-request.json @@ -0,0 +1,38 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "flatads": { + "token": "token", + "publisherId": "publisherId" + } + } + } + ], + "site": { + "page": "https://example.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "device": { + "ua": "test-user-agent", + "ip": "193.168.244.1", + "language": "en", + "dnt": 0 + }, + "tmax": 5000, + "regs": { + "gdpr": 0 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-response.json new file mode 100644 index 00000000000..2b648367e50 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-auction-flatads-response.json @@ -0,0 +1,44 @@ +{ + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "exp": 300, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner", + "meta":{ + "adaptercode": "flatads" + } + }, + "origbidcpm": 3.5 + } + } + ], + "seat": "flatads", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "flatads": "{{ flatads.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-request.json new file mode 100644 index 00000000000..7a01ef79dc7 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-request.json @@ -0,0 +1,65 @@ +{ + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id1", + "tagid": "ogTAGID", + "secure": 1, + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "tid": "${json-unit.any-string}", + "bidder": { + "token": "token", + "publisherId": "publisherId" + } + } + } + ], + "site": { + "domain": "example.com", + "page": "https://example.com", + "publisher": { + "domain": "example.com", + "id": "123456789" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "test-user-agent", + "ip": "193.168.244.1", + "language": "en", + "dnt": 0 + }, + "user": { + "buyeruid": "awesome-user" + }, + "at": 1, + "tmax": "${json-unit.any-number}", + "cur": [ + "USD" + ], + "source": { + "tid": "${json-unit.any-string}" + }, + "regs": { + "gdpr": 0 + }, + "ext": { + "prebid": { + "channel": { + "name": "web" + }, + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-response.json new file mode 100644 index 00000000000..adb7c87cd65 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/flatads/test-flatads-bid-response.json @@ -0,0 +1,27 @@ +{ + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties index d5b362a6f9b..747b9a2fc6a 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -235,6 +235,8 @@ adapters.evolution.endpoint=http://localhost:8090/evolution-exchange adapters.evtech.enabled=true adapters.feedad.enabled=true adapters.feedad.endpoint=http://localhost:8090/feedad-exchange +adapters.flatads.enabled=true +adapters.flatads.endpoint=http://localhost:8090/flatads-exchange adapters.flipp.enabled=true adapters.flipp.endpoint=http://localhost:8090/flipp-exchange adapters.audiencenetwork.enabled=true From 37d1e137554af502102b82e3c07847d9114ca7ae Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:44:43 -0400 Subject: [PATCH 50/51] Prebid Server prepare release 3.28.0 --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 7c9e884577f..2cf387c37e2 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.28.0-SNAPSHOT + 3.28.0 ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index 26c96b1fd81..dc3a2b08e9e 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index ae160a05610..efbb8dae9e0 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index d6974c7b64a..e32c8d8a528 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 29dd458e62a..a5c4519342c 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index 480193461c5..8753c997f24 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 045b0c53869..2992cb6cc55 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 027a7a5b6b0..4a32d6d3245 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.28.0 pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 84187c374ce..3f439c5b897 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.28.0-SNAPSHOT + 3.28.0 ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index 65bc83f7e6b..e5c96aa1d9c 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.28.0-SNAPSHOT + 3.28.0 pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - HEAD + 3.28.0 diff --git a/pom.xml b/pom.xml index 8afcb27fb36..7ad650f877a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.28.0-SNAPSHOT + 3.28.0 extra/pom.xml From 67042e509cf48c749ddfebbb9896edf3c5f516de Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:44:43 -0400 Subject: [PATCH 51/51] Prebid Server prepare for next development iteration --- extra/bundle/pom.xml | 2 +- extra/modules/confiant-ad-quality/pom.xml | 2 +- extra/modules/fiftyone-devicedetection/pom.xml | 2 +- extra/modules/greenbids-real-time-data/pom.xml | 2 +- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pb-request-correction/pom.xml | 2 +- extra/modules/pb-response-correction/pom.xml | 2 +- extra/modules/pb-richmedia-filter/pom.xml | 2 +- extra/modules/pom.xml | 2 +- extra/pom.xml | 4 ++-- pom.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 2cf387c37e2..aa4726813ed 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.28.0 + 3.29.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index dc3a2b08e9e..68df23c5221 100644 --- a/extra/modules/confiant-ad-quality/pom.xml +++ b/extra/modules/confiant-ad-quality/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT confiant-ad-quality diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml index efbb8dae9e0..f98b70c9aa9 100644 --- a/extra/modules/fiftyone-devicedetection/pom.xml +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT fiftyone-devicedetection diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml index e32c8d8a528..f80b8b1bb9e 100644 --- a/extra/modules/greenbids-real-time-data/pom.xml +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -4,7 +4,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT greenbids-real-time-data diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index a5c4519342c..a05ef20231b 100644 --- a/extra/modules/ortb2-blocking/pom.xml +++ b/extra/modules/ortb2-blocking/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml index 8753c997f24..2d0bdd57156 100644 --- a/extra/modules/pb-request-correction/pom.xml +++ b/extra/modules/pb-request-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT pb-request-correction diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml index 2992cb6cc55..4e6b21ca4cf 100644 --- a/extra/modules/pb-response-correction/pom.xml +++ b/extra/modules/pb-response-correction/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT pb-response-correction diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml index 4a32d6d3245..7076409c49a 100644 --- a/extra/modules/pb-richmedia-filter/pom.xml +++ b/extra/modules/pb-richmedia-filter/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0 + 3.29.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 3f439c5b897..307f5d7b204 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.28.0 + 3.29.0-SNAPSHOT ../../extra/pom.xml diff --git a/extra/pom.xml b/extra/pom.xml index e5c96aa1d9c..b0e7b78c044 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 3.28.0 + 3.29.0-SNAPSHOT pom https://github.com/prebid/prebid-server-java scm:git:git@github.com:prebid/prebid-server-java.git scm:git:git@github.com:prebid/prebid-server-java.git - 3.28.0 + HEAD diff --git a/pom.xml b/pom.xml index 7ad650f877a..b7573b1a282 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 3.28.0 + 3.29.0-SNAPSHOT extra/pom.xml