From 761ce9d03f0f3536cb14157e0175db9a7684567b Mon Sep 17 00:00:00 2001 From: Product AAX Date: Tue, 12 Apr 2022 19:51:21 +0530 Subject: [PATCH 01/12] Add AAX adapter --- .../prebid/server/bidder/aax/AaxBidder.java | 90 +++++++++++ .../config/bidder/AaxConfiguration.java | 47 ++++++ src/main/resources/bidder-config/aax.yaml | 21 +++ .../resources/static/bidder-params/aax.json | 20 +++ .../server/bidder/aax/AaxBidderTest.java | 145 ++++++++++++++++++ .../java/org/prebid/server/it/AaxTest.java | 36 +++++ .../it/openrtb2/aax/test-aax-bid-request.json | 42 +++++ .../openrtb2/aax/test-aax-bid-response.json | 22 +++ .../aax/test-auction-aax-request.json | 24 +++ .../aax/test-auction-aax-response.json | 38 +++++ .../server/it/test-application.properties | 2 + 11 files changed, 487 insertions(+) create mode 100644 src/main/java/org/prebid/server/bidder/aax/AaxBidder.java create mode 100644 src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java create mode 100644 src/main/resources/bidder-config/aax.yaml create mode 100644 src/main/resources/static/bidder-params/aax.json create mode 100644 src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java create mode 100644 src/test/java/org/prebid/server/it/AaxTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json diff --git a/src/main/java/org/prebid/server/bidder/aax/AaxBidder.java b/src/main/java/org/prebid/server/bidder/aax/AaxBidder.java new file mode 100644 index 00000000000..3fd232428b2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/aax/AaxBidder.java @@ -0,0 +1,90 @@ +package org.prebid.server.bidder.aax; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +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.HttpUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class AaxBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AaxBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + return Result.withValue(HttpRequest.builder() + .method(HttpMethod.POST) + .headers(HttpUtil.headers()) + .uri(endpointUrl) + .body(mapper.encodeToBytes(bidRequest)) + .payload(bidRequest) + .build()); + } + + @Override + public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { + if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { + return Collections.emptyList(); + } + + final String currency = bidResponse.getCur(); + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), currency)) + .collect(Collectors.toList()); + } + + private static BidType resolveBidType(String impId, List imps) { + for (Imp imp : imps) { + if (Objects.equals(impId, imp.getId())) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getAudio() != null) { + return BidType.audio; + } + } + } + + return BidType.banner; + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java new file mode 100644 index 00000000000..615c581adcc --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java @@ -0,0 +1,47 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.aax.AaxBidder; +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.util.HttpUtil; +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/aax.yaml", factory = YamlPropertySourceFactory.class) +public class AaxConfiguration { + + private static final String BIDDER_NAME = "aax"; + private static final String EXTERNAL_URL_MACRO = "{{PREBID_SERVER_ENDPOINT}}"; + + @Bean("aaxConfigurationProperties") + @ConfigurationProperties("adapters.aax") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps aaxBidderDeps(BidderConfigurationProperties aaxConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(aaxConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AaxBidder(resolveEndpoint(config.getEndpoint(), externalUrl), mapper)) + .assemble(); + } + + private String resolveEndpoint(String configEndpoint, String externalUrl) { + return configEndpoint.replace(EXTERNAL_URL_MACRO, HttpUtil.encodeUrl(externalUrl)); + } +} diff --git a/src/main/resources/bidder-config/aax.yaml b/src/main/resources/bidder-config/aax.yaml new file mode 100644 index 00000000000..e6bda296d94 --- /dev/null +++ b/src/main/resources/bidder-config/aax.yaml @@ -0,0 +1,21 @@ +adapters: + aax: + endpoint: https://prebid.aaxads.com/rtb/pb/aax-prebid?src={{PREBID_SERVER_ENDPOINT}} + meta-info: + maintainer-email: product@aax.media + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 720 + usersync: + url: https://c.aaxads.com/aacxc.php?fv=1&wbsh=psa&ryvlg=setstatuscode&redirect= + redirect-url: /setuid?bidder=aax&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&uid= + cookie-family-name: aax + type: redirect + support-cors: false diff --git a/src/main/resources/static/bidder-params/aax.json b/src/main/resources/static/bidder-params/aax.json new file mode 100644 index 00000000000..83cdfc59406 --- /dev/null +++ b/src/main/resources/static/bidder-params/aax.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Aax Adapter Params", + "description": "A schema which validates params accepted by the Aax adapter", + "type": "object", + "properties": { + "cid": { + "type": "string", + "description": "The customer id provided by AAX." + }, + "crid": { + "type": "string", + "description": "The placement id provided by AAX." + } + }, + "required": [ + "cid", + "crid" + ] +} diff --git a/src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java b/src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java new file mode 100644 index 00000000000..2ec81cc06c9 --- /dev/null +++ b/src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java @@ -0,0 +1,145 @@ +package org.prebid.server.bidder.aax; + +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.Before; +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.HttpCall; +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 java.util.List; +import java.util.function.Function; + +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.proto.openrtb.ext.response.BidType.banner; + +public class AaxBidderTest extends VertxTest { + + private static final String ENDPOINT_URL = "https://test.aax.net?src=external.prebidserver.com"; + + private AaxBidder aaxBidder; + + @Before + public void setup() { + aaxBidder = new AaxBidder(ENDPOINT_URL, jacksonMapper); + } + + @Test + public void creationShouldFailOnInvalidEndpointUrl() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AaxBidder("invalid_url", jacksonMapper)); + } + + @Test + public void makeHttpRequestsShouldNotModifyIncomingRequest() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final Result>> result; + result = aaxBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .containsExactly(bidRequest); + } + + @Test + public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { + // given + final HttpCall httpCall = sampleHttpCall(givenBidRequest(), "invalid response"); + + // when + final Result> result = aaxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .allMatch(error -> error.getType() == BidderError.Type.bad_server_response + && error.getMessage().startsWith("Failed to decode: Unrecognized token")); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall; + httpCall = sampleHttpCall(givenBidRequest(), mapper.writeValueAsString(null)); + + // when + final Result> result = aaxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { + // given + final HttpCall httpCall; + httpCall = sampleHttpCall(null, mapper.writeValueAsString(BidResponse.builder().build())); + + // when + final Result> result = aaxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException { + // given + final HttpCall httpCall = sampleHttpCall( + givenBidRequest(), + mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("123")))); + + // when + final Result> result = aaxBidder.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + } + + private static BidResponse sampleBidResponse(Function bidCustomizer) { + return BidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .build())) + .build(); + } + + private static HttpCall sampleHttpCall(BidRequest bidRequest, String body) { + return HttpCall.success( + HttpRequest.builder().payload(bidRequest).build(), + HttpResponse.of(200, null, body), + null); + } + + private static BidRequest givenBidRequest() { + return BidRequest.builder() + .id("request_id") + .imp(singletonList(Imp.builder() + .id("imp_id") + .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode()))) + .build())) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/it/AaxTest.java b/src/test/java/org/prebid/server/it/AaxTest.java new file mode 100644 index 00000000000..01f4061995f --- /dev/null +++ b/src/test/java/org/prebid/server/it/AaxTest.java @@ -0,0 +1,36 @@ +package org.prebid.server.it; + +import io.restassured.response.Response; +import org.json.JSONException; +import org.junit.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 AaxTest extends IntegrationTest { + + @Test + public void openrtb2AuctionShouldRespondWithBidsFromTheAax() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/aax-exchange")) + .withRequestBody(equalToJson(jsonFrom("openrtb2/aax/test-aax-bid-request.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/aax/test-aax-bid-response.json")))); + + // when + final Response response = responseFor("openrtb2/aax/test-auction-aax-request.json", + Endpoint.openrtb2_auction); + + // then + assertJsonEquals("openrtb2/aax/test-auction-aax-response.json", response, + singletonList("aax")); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json new file mode 100644 index 00000000000..ba09e5f9846 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-request.json @@ -0,0 +1,42 @@ +{ + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "cid": "AAXCID", + "crid": "12345678" + } + } + } + ], + "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": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json new file mode 100644 index 00000000000..4b1ecda6ece --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-aax-bid-response.json @@ -0,0 +1,22 @@ + { + "id": "tid", + "seatbid": [ + { + "seat": "aax", + "bid": [ + { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + } + ] + } + ], + "bidid": "bid01" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json new file mode 100644 index 00000000000..7e76123f01b --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-request.json @@ -0,0 +1,24 @@ +{ + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "aax": { + "cid": "AAXCID", + "crid": "12345678" + } + } + } + ], + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json new file mode 100644 index 00000000000..510834b5e2e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json @@ -0,0 +1,38 @@ +{ + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 0.5 + } + } + ], + "seat": "aax", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "aax": "{{ aax.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 d9f05a99a54..825bcc910e3 100644 --- a/src/test/resources/org/prebid/server/it/test-application.properties +++ b/src/test/resources/org/prebid/server/it/test-application.properties @@ -288,6 +288,8 @@ 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.aax.enabled=true +adapters.aax.endpoint=http://localhost:8090/aax-exchange http-client.circuit-breaker.enabled=true http-client.circuit-breaker.idle-expire-hours=24 http-client.circuit-breaker.opening-threshold=1 From 43267f778f07ef04c4c95254ce144b9e0ba773e7 Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Sat, 16 Apr 2022 21:19:52 +0300 Subject: [PATCH 02/12] Price Floors: Feature enhancements (#1820) --- .../server/bidder/model/BidderError.java | 9 + .../server/bidder/rubicon/RubiconBidder.java | 113 ++-- .../floors/BasicPriceFloorEnforcer.java | 4 +- .../floors/BasicPriceFloorProcessor.java | 89 ++- .../floors/BasicPriceFloorResolver.java | 16 +- .../server/floors/PriceFloorFetcher.java | 47 +- .../server/floors/PriceFloorResolver.java | 10 +- .../floors/PriceFloorRulesValidator.java | 91 +++ .../pricefloors/PriceFloorsBaseSpec.groovy | 2 +- .../PriceFloorsEnforcementSpec.groovy | 4 +- .../PriceFloorsFetchingSpec.groovy | 589 +++++++++++++++++- .../PriceFloorsSignalingSpec.groovy | 26 +- .../bidder/rubicon/RubiconBidderTest.java | 70 ++- .../floors/BasicPriceFloorEnforcerTest.java | 2 +- .../floors/BasicPriceFloorProcessorTest.java | 98 ++- .../floors/BasicPriceFloorResolverTest.java | 276 ++++---- 16 files changed, 1129 insertions(+), 317 deletions(-) create mode 100644 src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java diff --git a/src/main/java/org/prebid/server/bidder/model/BidderError.java b/src/main/java/org/prebid/server/bidder/model/BidderError.java index 3f3facd0cc4..620874ece50 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderError.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderError.java @@ -34,6 +34,10 @@ public static BidderError badServerResponse(String message) { return BidderError.of(message, Type.bad_server_response); } + public static BidderError rejectedIpf(String message) { + return BidderError.of(message, Type.rejected_ipf); + } + public static BidderError failedToRequestBids(String message) { return BidderError.of(message, Type.failed_to_request_bids); } @@ -79,6 +83,11 @@ public enum Type { */ invalid_bid(5), + /** + * Covers the case where a bid was rejected by price-floors feature functionality + */ + rejected_ipf(6), + timeout(1), generic(999); 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 8a1c38c3235..a9136e90886 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -38,6 +38,7 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; +import org.prebid.server.bidder.model.PriceFloorInfo; import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.rubicon.proto.request.RubiconAppExt; import org.prebid.server.bidder.rubicon.proto.request.RubiconBannerExt; @@ -67,8 +68,6 @@ import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.PriceFloorResolver; -import org.prebid.server.floors.model.PriceFloorData; -import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.json.DecodeException; @@ -418,14 +417,31 @@ private Imp makeImp(Imp imp, final PriceFloorResult priceFloorResult = resolvePriceFloors(bidRequest, imp, isVideo ? ImpMediaType.video : ImpMediaType.banner, priceFloorsWarnings); + final BigDecimal ipfFloor = ObjectUtil.getIfNotNull(priceFloorResult, PriceFloorResult::getFloorValue); + final String ipfCurrency = ipfFloor != null + ? resolveCurrencyFromFloorResult( + ObjectUtil.getIfNotNull(priceFloorResult, PriceFloorResult::getCurrency), + bidRequest, + imp, + errors) + : null; + final Imp.ImpBuilder builder = imp.toBuilder() .metric(makeMetrics(imp)) .ext(mapper.mapper().valueToTree( - makeImpExt(imp, extImpRubicon, site, app, extRequest, priceFloorResult))); - - final BigDecimal resolvedBidFloor = priceFloorResult != null - ? resolvePriceFloorBidFloor(imp, bidRequest, priceFloorResult, errors) - : resolveBidFloor(imp, bidRequest, errors); + makeImpExt( + imp, + bidRequest, + extImpRubicon, + site, + app, + extRequest, + ipfCurrency, + priceFloorResult))); + + final BigDecimal resolvedBidFloor = ipfFloor != null + ? convertToXAPICurrency(ipfFloor, ipfCurrency, imp, bidRequest) + : resolveBidFloorFromImp(imp, bidRequest, errors); if (resolvedBidFloor != null) { builder @@ -455,21 +471,16 @@ private PriceFloorResult resolvePriceFloors(BidRequest bidRequest, return floorResolver.resolve( bidRequest, - extractFloorModelGroup(bidRequest), + extractFloorRules(bidRequest), imp, mediaType, null, warnings); } - private static PriceFloorModelGroup extractFloorModelGroup(BidRequest bidRequest) { + private static PriceFloorRules extractFloorRules(BidRequest bidRequest) { final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); - final PriceFloorRules floorRules = ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); - - final PriceFloorData data = ObjectUtil.getIfNotNull(floorRules, PriceFloorRules::getData); - final List modelGroups = ObjectUtil.getIfNotNull(data, PriceFloorData::getModelGroups); - - return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.get(0) : null; + return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); } private List makeMetrics(Imp imp) { @@ -495,31 +506,27 @@ private boolean isMetricSupported(Metric metric) { return supportedVendors.contains(metric.getVendor()) && Objects.equals(metric.getType(), "viewability"); } - private BigDecimal resolvePriceFloorBidFloor(Imp imp, - BidRequest bidRequest, - PriceFloorResult floorResult, - List errors) { - final BigDecimal floorValue = ObjectUtil.getIfNotNull(floorResult, PriceFloorResult::getFloorValue); - if (floorValue == null) { + private BigDecimal resolveBidFloorFromImp(Imp imp, BidRequest bidRequest, List errors) { + final BigDecimal resolvedBidFloorPrice = resolveBidFloorPrice(imp); + if (resolvedBidFloorPrice == null) { return null; } - final String floorCurrency = resolveCurrencyFromFloorResult(floorResult, bidRequest, imp, errors); - return ObjectUtils.notEqual(floorCurrency, XAPI_CURRENCY) - ? convertBidFloorCurrency(floorValue, floorCurrency, imp, bidRequest) - : null; + return convertToXAPICurrency( + resolvedBidFloorPrice, + resolveBidFloorCurrency(imp, bidRequest, errors), + imp, + bidRequest); } - private BigDecimal resolveBidFloor(Imp imp, BidRequest bidRequest, List errors) { - final BigDecimal resolvedBidFloorPrice = resolveBidFloorPrice(imp); - if (resolvedBidFloorPrice == null) { - return null; - } + private BigDecimal convertToXAPICurrency(BigDecimal value, + String fromCurrency, + Imp imp, + BidRequest bidRequest) { - final String resolvedBidFloorCurrency = resolveBidFloorCurrency(imp, bidRequest, errors); - return ObjectUtils.notEqual(resolvedBidFloorCurrency, XAPI_CURRENCY) - ? convertBidFloorCurrency(resolvedBidFloorPrice, resolvedBidFloorCurrency, imp, bidRequest) - : null; + return ObjectUtils.notEqual(fromCurrency, XAPI_CURRENCY) + ? convertBidFloorCurrency(value, fromCurrency, imp, bidRequest) + : value; } private static BigDecimal resolveBidFloorPrice(Imp imp) { @@ -539,20 +546,19 @@ private static String resolveBidFloorCurrency(Imp imp, BidRequest bidRequest, Li return bidFloorCurrency; } - private static String resolveCurrencyFromFloorResult(PriceFloorResult floorResult, + private static String resolveCurrencyFromFloorResult(String floorCurrency, BidRequest bidRequest, Imp imp, List errors) { - final String bidFloorCurrency = floorResult.getCurrency(); - if (StringUtils.isBlank(bidFloorCurrency)) { + if (StringUtils.isBlank(floorCurrency)) { if (isDebugEnabled(bidRequest)) { - errors.add(BidderError.badInput(String.format("Imp `%s` floor provided with no currency, assuming %s", - imp.getId(), XAPI_CURRENCY))); + errors.add(BidderError.badInput(String.format("Ipf for imp `%s` provided floor with no currency, " + + "assuming %s", imp.getId(), XAPI_CURRENCY))); } return XAPI_CURRENCY; } - return bidFloorCurrency; + return floorCurrency; } /** @@ -581,15 +587,17 @@ private BigDecimal convertBidFloorCurrency(BigDecimal bidFloor, } private RubiconImpExt makeImpExt(Imp imp, + BidRequest bidRequest, ExtImpRubicon rubiconImpExt, Site site, App app, ExtRequest extRequest, + String ipfResolvedCurrency, PriceFloorResult priceFloorResult) { final ExtImpContext context = extImpContext(imp); final RubiconImpExtPrebid rubiconImpExtPrebid = priceFloorResult != null - ? makeRubiconExtPrebid(priceFloorResult) + ? makeRubiconExtPrebid(priceFloorResult, ipfResolvedCurrency, imp, bidRequest) : null; return RubiconImpExt.of( RubiconImpExtRp.of( @@ -626,11 +634,14 @@ private JsonNode makeTarget(Imp imp, ExtImpRubicon rubiconImpExt, Site site, App return result.size() > 0 ? result : null; } - private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult) { + private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult, + String currency, + Imp imp, + BidRequest bidRequest) { return RubiconImpExtPrebid.of(ExtImpPrebidFloors.of( priceFloorResult.getFloorRule(), - priceFloorResult.getFloorRuleValue(), - priceFloorResult.getFloorValue())); + convertToXAPICurrency(priceFloorResult.getFloorRuleValue(), currency, imp, bidRequest), + convertToXAPICurrency(priceFloorResult.getFloorValue(), currency, imp, bidRequest))); } private void mergeFirstPartyDataFromSite(Site site, ObjectNode result) { @@ -1477,6 +1488,8 @@ private List bidsFromResponse(BidRequest prebidRequest, List errors) { final Map idToImp = prebidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, Function.identity())); + final Map idToRubiconImp = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); final Float cpmOverrideFromRequest = cpmOverrideFromRequest(prebidRequest); final BidType bidType = bidType(bidRequest); @@ -1487,7 +1500,7 @@ private List bidsFromResponse(BidRequest prebidRequest, .filter(Objects::nonNull) .flatMap(Collection::stream) .map(bid -> updateBid(bid, idToImp.get(bid.getImpid()), cpmOverrideFromRequest, bidResponse)) - .map(bid -> BidderBid.of(bid, bidType, bidResponse.getCur())) + .map(bid -> createBidderBid(bid, idToRubiconImp.get(bid.getImpid()), bidType, bidResponse.getCur())) .collect(Collectors.toList()); } @@ -1561,6 +1574,16 @@ private Bid updateBid(Bid bid, Imp imp, Float cpmOverrideFromRequest, RubiconBid .build(); } + private static BidderBid createBidderBid(Bid bid, Imp imp, BidType bidType, String currency) { + + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(currency) + .priceFloorInfo(imp != null ? PriceFloorInfo.of(imp.getBidfloor(), imp.getBidfloorcur()) : null) + .build(); + } + private Float cpmOverrideFromRequest(BidRequest bidRequest) { final RubiconExtPrebidBiddersBidder bidder = extPrebidBiddersRubicon(bidRequest.getExt()); final RubiconExtPrebidBiddersBidderDebug debug = bidder != null ? bidder.getDebug() : null; diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 70c2077f226..07901ed704c 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -135,7 +135,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, final List updatedBidderBids = new ArrayList<>(bidderBids); final List errors = new ArrayList<>(seatBid.getErrors()); - final List warnings = new ArrayList<>(seatBid.getErrors()); + final List warnings = new ArrayList<>(seatBid.getWarnings()); final BidRequest bidderBidRequest = auctionParticipation.getBidderRequest().getBidRequest(); final PriceFloorRules floors = extractFloors(auctionParticipation); @@ -153,7 +153,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, final BigDecimal floor = resolveFloor(bidderBid, bidderBidRequest, bidRequest, errors); if (isPriceBelowFloor(price, floor)) { - warnings.add(BidderError.generic( + warnings.add(BidderError.rejectedIpf( String.format("Bid with id '%s' was rejected by floor enforcement: " + "price %s is below the floor %s", bid.getId(), price, floor))); diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index eff08b6b360..0c1f0efa2c6 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; @@ -11,6 +13,7 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.model.Price; import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorLocation; @@ -20,6 +23,7 @@ import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; @@ -39,9 +43,12 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { + private static final Logger logger = LoggerFactory.getLogger(BasicPriceFloorProcessor.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; - private static final int MODEL_WEIGHT_MAX_VALUE = 1_000_000; + private static final int MODEL_WEIGHT_MAX_VALUE = 100; private static final int MODEL_WEIGHT_MIN_VALUE = 0; private final PriceFloorFetcher floorFetcher; @@ -68,10 +75,10 @@ public AuctionContext enrichWithPriceFloors(AuctionContext auctionContext) { final List warnings = auctionContext.getDebugWarnings(); if (isPriceFloorsDisabled(account, bidRequest)) { - return auctionContext; + return auctionContext.with(disableFloorsForRequest(bidRequest)); } - final PriceFloorRules floors = resolveFloors(account, bidRequest); + final PriceFloorRules floors = resolveFloors(account, bidRequest, errors); final BidRequest updatedBidRequest = updateBidRequestWithFloors(bidRequest, floors, errors, warnings); return auctionContext.with(updatedBidRequest); @@ -81,6 +88,22 @@ private boolean isPriceFloorsDisabled(Account account, BidRequest bidRequest) { return isPriceFloorsDisabledForAccount(account) || isPriceFloorsDisabledForRequest(bidRequest); } + private static BidRequest disableFloorsForRequest(BidRequest bidRequest) { + final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); + final PriceFloorRules rules = ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); + + final PriceFloorRules updatedRules = (rules != null ? rules.toBuilder() : PriceFloorRules.builder()) + .enabled(false) + .build(); + final ExtRequestPrebid updatedPrebid = (prebid != null ? prebid.toBuilder() : ExtRequestPrebid.builder()) + .floors(updatedRules) + .build(); + + return bidRequest.toBuilder() + .ext(ExtRequest.of(updatedPrebid)) + .build(); + } + private static boolean isPriceFloorsDisabledForAccount(Account account) { final AccountPriceFloorsConfig priceFloors = ObjectUtil.getIfNotNull(account.getAuction(), AccountAuctionConfig::getPriceFloors); @@ -97,7 +120,7 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) { return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); } - private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest) { + private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List errors) { final PriceFloorRules requestFloors = extractRequestFloors(bidRequest); final FetchResult fetchResult = floorFetcher.fetch(account); @@ -109,7 +132,18 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest) { } if (requestFloors != null) { - return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); + try { + PriceFloorRulesValidator.validate(requestFloors, Integer.MAX_VALUE); + return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); + } catch (PreBidException e) { + errors.add(String.format("Failed to parse price floors from request," + + " with a reason : %s ", e.getMessage())); + conditionalLogger.error( + String.format("Failed to parse price floors from request with id: '%s'," + + " with a reason : %s ", + bidRequest.getId(), + e.getMessage()), 0.01d); + } } return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData); @@ -231,6 +265,7 @@ private static PriceFloorRules createFloorsFrom(PriceFloorRules floors, final PriceFloorData updatedFloorData = floorData != null ? updateFloorData(floorData) : null; return (floors != null ? floors.toBuilder() : PriceFloorRules.builder()) + .floorProvider(resolveFloorProvider(floors)) .fetchStatus(fetchStatus) .location(location) .data(updatedFloorData) @@ -289,16 +324,27 @@ private static int resolveModelGroupWeight(PriceFloorModelGroup modelGroup) { return ObjectUtils.defaultIfNull(modelGroup.getModelWeight(), 1); } + private static String resolveFloorProvider(PriceFloorRules rules) { + final PriceFloorData floorData = ObjectUtil.getIfNotNull(rules, PriceFloorRules::getData); + final String dataLevelProvider = ObjectUtil.getIfNotNull(floorData, PriceFloorData::getFloorProvider); + + return StringUtils.isNotBlank(dataLevelProvider) + ? dataLevelProvider + : ObjectUtil.getIfNotNull(rules, PriceFloorRules::getFloorProvider); + } + private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, PriceFloorRules floors, List errors, List warnings) { - final boolean skipFloors = shouldSkipFloors(floors); + + final Integer requestSkipRate = extractSkipRate(floors); + final boolean skipFloors = shouldSkipFloors(requestSkipRate); final List imps = skipFloors ? bidRequest.getImp() : updateImpsWithFloors(floors, bidRequest, errors, warnings); - final ExtRequest extRequest = updateExtRequestWithFloors(bidRequest, floors, skipFloors); + final ExtRequest extRequest = updateExtRequestWithFloors(bidRequest, floors, requestSkipRate, skipFloors); return bidRequest.toBuilder() .imp(imps) @@ -306,9 +352,7 @@ private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, .build(); } - private static boolean shouldSkipFloors(PriceFloorRules floors) { - final Integer skipRate = extractSkipRate(floors); - + private static boolean shouldSkipFloors(Integer skipRate) { return skipRate != null && ThreadLocalRandom.current().nextInt(SKIP_RATE_MAX) < skipRate; } @@ -337,7 +381,7 @@ private static boolean isValidSkipRate(Integer value) { return value != null && value >= SKIP_RATE_MIN && value <= SKIP_RATE_MAX; } - private List updateImpsWithFloors(PriceFloorRules accountFloors, + private List updateImpsWithFloors(PriceFloorRules effectiveFloors, BidRequest bidRequest, List errors, List warnings) { @@ -346,14 +390,15 @@ private List updateImpsWithFloors(PriceFloorRules accountFloors, final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); final PriceFloorRules floors = - ObjectUtils.defaultIfNull(accountFloors, ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors)); + ObjectUtils.defaultIfNull(effectiveFloors, + ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors)); final PriceFloorModelGroup modelGroup = extractFloorModelGroup(floors); if (modelGroup == null) { return imps; } return CollectionUtils.emptyIfNull(imps).stream() - .map(imp -> updateImpWithFloors(imp, modelGroup, bidRequest, errors, warnings)) + .map(imp -> updateImpWithFloors(imp, floors, bidRequest, errors, warnings)) .collect(Collectors.toList()); } @@ -365,14 +410,14 @@ private static PriceFloorModelGroup extractFloorModelGroup(PriceFloorRules floor } private Imp updateImpWithFloors(Imp imp, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, BidRequest bidRequest, List errors, List warnings) { final PriceFloorResult priceFloorResult; try { - priceFloorResult = floorResolver.resolve(bidRequest, modelGroup, imp, warnings); + priceFloorResult = floorResolver.resolve(bidRequest, floorRules, imp, warnings); } catch (IllegalStateException e) { errors.add("Cannot resolve bid floor, error: " + e.getMessage()); return imp; @@ -404,19 +449,29 @@ private ObjectNode updateImpExtWithFloors(ObjectNode ext, PriceFloorResult price private static ExtRequest updateExtRequestWithFloors(BidRequest bidRequest, PriceFloorRules floors, + Integer skipRate, boolean skipFloors) { final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(bidRequest.getExt(), ExtRequest::getPrebid); final ExtRequestPrebid updatedPrebid = (prebid != null ? prebid.toBuilder() : ExtRequestPrebid.builder()) - .floors(skipFloors ? skippedFloors(floors) : floors) + .floors(skipFloors ? skippedFloors(floors, skipRate) : enabledFloors(floors, skipRate)) .build(); return ExtRequest.of(updatedPrebid); } - private static PriceFloorRules skippedFloors(PriceFloorRules floors) { + private static PriceFloorRules enabledFloors(PriceFloorRules floors, Integer skipRate) { + return floors.toBuilder() + .skipRate(skipRate) + .enabled(true) + .build(); + } + + private static PriceFloorRules skippedFloors(PriceFloorRules floors, Integer skipRate) { return floors.toBuilder() + .skipRate(skipRate) + .enabled(false) .skipped(true) .build(); } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java index 167a83c6a27..3adb5e3398d 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorResolver.java @@ -93,12 +93,14 @@ public BasicPriceFloorResolver(CurrencyConversionService currencyConversionServi @Override public PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, ImpMediaType mediaType, Format format, List warnings) { + final PriceFloorModelGroup modelGroup = extractFloorModelGroup(floorRules); + if (modelGroup == null) { return null; } @@ -124,7 +126,7 @@ public PriceFloorResult resolve(BidRequest bidRequest, final String modelGroupCurrency = modelGroup.getCurrency(); final String floorCurrency = StringUtils.isNotEmpty(modelGroupCurrency) ? modelGroupCurrency - : getDataCurrency(bidRequest); + : getDataCurrency(floorRules); try { return resolveResult(floor, rule, floorForRule, bidRequest, floorCurrency); @@ -143,6 +145,13 @@ public PriceFloorResult resolve(BidRequest bidRequest, return null; } + private static PriceFloorModelGroup extractFloorModelGroup(PriceFloorRules floors) { + final PriceFloorData data = ObjectUtil.getIfNotNull(floors, PriceFloorRules::getData); + final List modelGroups = ObjectUtil.getIfNotNull(data, PriceFloorData::getModelGroups); + + return CollectionUtils.isNotEmpty(modelGroups) ? modelGroups.get(0) : null; + } + private List> createRuleKey(PriceFloorSchema schema, BidRequest bidRequest, Imp imp, @@ -427,8 +436,7 @@ private static Map keysToLowerCase(Map map) { .collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue)); } - private static String getDataCurrency(BidRequest bidRequest) { - final PriceFloorRules rules = extractRules(bidRequest); + private static String getDataCurrency(PriceFloorRules rules) { final PriceFloorData data = ObjectUtil.getIfNotNull(rules, PriceFloorRules::getData); return ObjectUtil.getIfNotNull(data, PriceFloorData::getCurrency); diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index c2615369f36..e0bb90ae395 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -9,8 +9,6 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import lombok.Value; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -18,10 +16,8 @@ import org.apache.http.HttpStatus; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.floors.model.PriceFloorData; -import org.prebid.server.floors.model.PriceFloorModelGroup; -import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.floors.model.PriceFloorDebugProperties; +import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.json.DecodeException; @@ -38,7 +34,6 @@ import org.prebid.server.vertx.http.HttpClient; import org.prebid.server.vertx.http.model.HttpClientResponse; -import java.math.BigDecimal; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -175,8 +170,7 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon } final PriceFloorRules priceFloorRules = parsePriceFloorRules(body, accountId); - - validatePriceFloorRules(priceFloorRules, fetchConfig); + PriceFloorRulesValidator.validate(priceFloorRules, resolveMaxRules(fetchConfig.getMaxRules())); return ResponseCacheInfo.of(priceFloorRules, FetchStatus.success, @@ -195,39 +189,10 @@ private PriceFloorRules parsePriceFloorRules(String body, String accountId) { return priceFloorRules; } - private void validatePriceFloorRules(PriceFloorRules priceFloorRules, AccountPriceFloorsFetchConfig fetchConfig) { - final PriceFloorData data = priceFloorRules.getData(); - - if (data == null) { - throw new PreBidException("Price floor rules data must be present"); - } - - if (CollectionUtils.isEmpty(data.getModelGroups())) { - throw new PreBidException("Price floor rules should contain at least one model group"); - } - - final int maxRules = resolveMaxRules(Math.toIntExact(fetchConfig.getMaxRules())); - - CollectionUtils.emptyIfNull(data.getModelGroups()).stream() - .filter(Objects::nonNull) - .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); - } - - private static int resolveMaxRules(Integer accountMaxRules) { - return Objects.equals(accountMaxRules, 0) ? Integer.MAX_VALUE : accountMaxRules; - } - - private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) { - final Map values = modelGroup.getValues(); - if (MapUtils.isEmpty(values)) { - throw new PreBidException(String.format("Price floor rules values can't be null or empty, but were %s", - values)); - } - - if (values.size() > maxRules) { - throw new PreBidException(String.format("Price floor rules number %s exceeded its maximum number %s", - values.size(), maxRules)); - } + private static int resolveMaxRules(Long accountMaxRules) { + return accountMaxRules != null && !accountMaxRules.equals(0L) + ? Math.toIntExact(accountMaxRules) + : Integer.MAX_VALUE; } private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String fetchUrl) { diff --git a/src/main/java/org/prebid/server/floors/PriceFloorResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorResolver.java index 601d875343d..3b596027a9f 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorResolver.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorResolver.java @@ -3,8 +3,8 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; -import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; +import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.util.List; @@ -12,18 +12,18 @@ public interface PriceFloorResolver { PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, ImpMediaType mediaType, Format format, List warnings); default PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, List warnings) { - return resolve(bidRequest, modelGroup, imp, null, null, warnings); + return resolve(bidRequest, floorRules, imp, null, null, warnings); } static NoOpPriceFloorResolver noOp() { @@ -34,7 +34,7 @@ class NoOpPriceFloorResolver implements PriceFloorResolver { @Override public PriceFloorResult resolve(BidRequest bidRequest, - PriceFloorModelGroup modelGroup, + PriceFloorRules floorRules, Imp imp, ImpMediaType mediaType, Format format, diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java new file mode 100644 index 00000000000..e61d6bd43de --- /dev/null +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -0,0 +1,91 @@ +package org.prebid.server.floors; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorModelGroup; +import org.prebid.server.floors.model.PriceFloorRules; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Objects; + +public class PriceFloorRulesValidator { + + private static final int MODEL_WEIGHT_MAX_VALUE = 100; + private static final int MODEL_WEIGHT_MIN_VALUE = 1; + private static final int SKIP_RATE_MIN = 0; + private static final int SKIP_RATE_MAX = 100; + + private PriceFloorRulesValidator() { + } + + public static void validate(PriceFloorRules priceFloorRules, Integer maxRules) { + + final Integer rootSkipRate = priceFloorRules.getSkipRate(); + if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) { + throw new PreBidException(String.format("Price floor root skipRate " + + "must be in range(0-100), but was %s", rootSkipRate)); + } + + final BigDecimal floorMin = priceFloorRules.getFloorMin(); + if (floorMin != null && floorMin.compareTo(BigDecimal.ZERO) < 0) { + throw new PreBidException(String.format("Price floor floorMin " + + "must be positive float, but was %s", floorMin)); + } + + final PriceFloorData data = priceFloorRules.getData(); + if (data == null) { + throw new PreBidException("Price floor rules data must be present"); + } + + final Integer dataSkipRate = data.getSkipRate(); + if (dataSkipRate != null && (dataSkipRate < SKIP_RATE_MIN || dataSkipRate > SKIP_RATE_MAX)) { + throw new PreBidException(String.format("Price floor data skipRate " + + "must be in range(0-100), but was %s", dataSkipRate)); + } + + if (CollectionUtils.isEmpty(data.getModelGroups())) { + throw new PreBidException("Price floor rules should contain at least one model group"); + } + + CollectionUtils.emptyIfNull(data.getModelGroups()).stream() + .filter(Objects::nonNull) + .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); + } + + private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) { + final Integer modelWeight = modelGroup.getModelWeight(); + if (modelWeight != null + && (modelWeight < MODEL_WEIGHT_MIN_VALUE || modelWeight > MODEL_WEIGHT_MAX_VALUE)) { + + throw new PreBidException(String.format("Price floor modelGroup modelWeight " + + "must be in range(1-100), but was %s", modelWeight)); + + } + + final Integer skipRate = modelGroup.getSkipRate(); + if (skipRate != null && (skipRate < SKIP_RATE_MIN || skipRate > SKIP_RATE_MAX)) { + throw new PreBidException(String.format("Price floor modelGroup skipRate " + + "must be in range(0-100), but was %s", skipRate)); + } + + final BigDecimal defaultPrice = modelGroup.getDefaultFloor(); + if (defaultPrice != null && defaultPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new PreBidException(String.format("Price floor modelGroup default " + + "must be positive float, but was %s", defaultPrice)); + } + + final Map values = modelGroup.getValues(); + if (MapUtils.isEmpty(values)) { + throw new PreBidException(String.format("Price floor rules values can't be null or empty, but were %s", + values)); + } + + if (values.size() > maxRules) { + throw new PreBidException(String.format("Price floor rules number %s exceeded its maximum number %s", + values.size(), maxRules)); + } + } +} 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 edc6953fbfd..cedcffc37d4 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 @@ -39,8 +39,8 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { FloorsProvider.FLOORS_ENDPOINT protected static final FloorsProvider floorsProvider = new FloorsProvider(Dependencies.networkServiceContainer, Dependencies.objectMapperWrapper) + protected static final int MAX_MODEL_WEIGHT = 100 private static final int DEFAULT_MODEL_WEIGHT = 1 - private static final int MAX_MODEL_WEIGHT = 1000000 private static final int CURRENCY_CONVERSION_PRECISION = 3 private static final int FLOOR_VALUE_PRECISION = 4 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 f5c5a1ff711..617a8a78b61 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 @@ -79,7 +79,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { } and: "PBS should log warning about bid suppression" - assert response.ext?.warnings[ErrorType.GENERIC_ALIAS]*.code == [999] + assert response.ext?.warnings[ErrorType.GENERIC_ALIAS]*.code == [6] assert response.ext?.warnings[ErrorType.GENERIC_ALIAS]*.message == ["Bid with id '${aliasBidResponse.seatbid[0].bid[0].id}' was rejected by floor enforcement: " + "price $lowerPrice is below the floor $floorValue" as String] @@ -128,7 +128,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { assert response.seatbid?.first()?.bid?.collect { it.price } == [floorValue] and: "PBS should log warning about suppression all bids below the floor value " - assert response.ext?.warnings[ErrorType.GENERIC]*.code == [999, 999] + assert response.ext?.warnings[ErrorType.GENERIC]*.code == [6, 6] assert response.ext?.warnings[ErrorType.GENERIC]*.message == ["Bid with id '${bidResponse.seatbid[0].bid[1].id}' was rejected by floor enforcement: " + "price ${bidResponse.seatbid[0].bid[1].price} is below the floor $floorValue" as String, diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 9781999348f..a92088e143d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -15,23 +15,30 @@ import org.prebid.server.functional.util.PBSUtils import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH import static org.prebid.server.functional.model.request.auction.Location.REQUEST +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { - private static final int maxEnforceFloorsRate = 100 - + private static final int MAX_ENFORCE_FLOORS_RATE = 100 private static final int DEFAULT_MAX_AGE_SEC = 600 private static final int DEFAULT_PERIOD_SEC = 300 private static final int MIN_TIMEOUT_MS = 10 private static final int MAX_TIMEOUT_MS = 10000 + private static final int MIN_SKIP_RATE = 0 + private static final int MAX_SKIP_RATE = 100 + private static final int MIN_DEFAULT_FLOOR_VALUE = 0 + private static final int MIN_FLOOR_MIN = 0 private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure" @@ -256,7 +263,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert !response.seatbid?.isEmpty() where: - enforceFloorsRate << [PBSUtils.randomNegativeNumber, maxEnforceFloorsRate + 1] + enforceFloorsRate << [PBSUtils.randomNegativeNumber, MAX_ENFORCE_FLOORS_RATE + 1] } def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() { @@ -694,7 +701,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Account with maxFileSizeKb in the DB" def accountId = bidRequest.app.publisher.id - def maxSize = PBSUtils.getRandomNumber(0, 10) + def maxSize = PBSUtils.getRandomNumber(1, 10) def account = getAccountWithEnabledFetch(accountId).tap { config.auction.priceFloors.fetch.maxFileSizeKb = maxSize } @@ -764,7 +771,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == storedRequestModel.ext.prebid.floors.floorMin - ext?.prebid?.floors?.floorProvider == storedRequestModel.ext.prebid.floors.floorProvider + ext?.prebid?.floors?.floorProvider == storedRequestModel.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == storedRequestModel.ext.prebid.floors.data } @@ -807,7 +814,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == bidRequest.ext.prebid.floors.floorMin - ext?.prebid?.floors?.floorProvider == bidRequest.ext.prebid.floors.floorProvider + ext?.prebid?.floors?.floorProvider == bidRequest.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == bidRequest.ext.prebid.floors.data } } @@ -846,7 +853,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == ampStoredRequest.ext.prebid.floors.floorMin - ext?.prebid?.floors?.floorProvider == ampStoredRequest.ext.prebid.floors.floorProvider + ext?.prebid?.floors?.floorProvider == ampStoredRequest.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == ampStoredRequest.ext.prebid.floors.data } } @@ -891,7 +898,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse.data @@ -934,7 +941,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse.data @@ -1020,21 +1027,54 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse.data } } + def "PBS should validate rules from request when floorMin from request is invalid"() { + given: "Default BidRequest with floorMin" + def floorValue = PBSUtils.randomFloorValue + def invalidFloorMin = MIN_FLOOR_MIN - 1 + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.floorMin = invalidFloorMin + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor floorMin " + + "must be positive float, but was $invalidFloorMin "] + } + def "PBS should validate rules from request when modelWeight from request is invalid"() { given: "Default BidRequest with floors" def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight - ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue] + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] ext.prebid.floors.data.modelGroups.last().modelWeight = modelWeight } @@ -1045,14 +1085,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + def response = floorsPbsService.sendAuctionRequest(bidRequest) - then: "Bidder request bidFloor should correspond to valid modelGroup" + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == floorValue + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " + + "must be in range(1-100), but was $invalidModelWeight "] where: - invalidModelWeight << [0, -1, 1000000] + invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] } def "PBS should validate rules from amp request when modelWeight from request is invalid"() { @@ -1062,10 +1107,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Default stored request with floors" def floorValue = PBSUtils.randomFloorValue def ampStoredRequest = storedRequestWithFloors.tap { + imp[0].bidFloor = floorValue ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight - ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue] + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] ext.prebid.floors.data.modelGroups.last().modelWeight = modelWeight } def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) @@ -1078,14 +1124,176 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) when: "PBS processes auction request" - floorsPbsService.sendAmpRequest(ampRequest) + def response = floorsPbsService.sendAmpRequest(ampRequest) then: "Bidder request bidFloor should correspond to valid modelGroup" - def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " + + "must be in range(1-100), but was $invalidModelWeight "] + where: - invalidModelWeight << [0, -1, 1000000] + invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] + } + + def "PBS should reject fetch when root skipRate from request is invalid"() { + given: "Default BidRequest with skipRate" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].skipRate = 0 + ext.prebid.floors.data.skipRate = 0 + ext.prebid.floors.skipRate = invalidSkipRate + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().skipRate = 0 + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor root skipRate " + + "must be in range(0-100), but was $invalidSkipRate "] + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when data skipRate from request is invalid"() { + given: "Default BidRequest with skipRate" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].skipRate = 0 + ext.prebid.floors.data.skipRate = invalidSkipRate + ext.prebid.floors.skipRate = 0 + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().skipRate = 0 + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor data skipRate " + + "must be in range(0-100), but was $invalidSkipRate "] + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when modelGroup skipRate from request is invalid"() { + given: "Default BidRequest with skipRate" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].skipRate = invalidSkipRate + ext.prebid.floors.data.skipRate = 0 + ext.prebid.floors.skipRate = 0 + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().skipRate = 0 + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup skipRate " + + "must be in range(0-100), but was $invalidSkipRate "] + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should validate rules from request when default floor value from request is invalid"() { + given: "Default BidRequest with default floor value" + def floorValue = PBSUtils.randomFloorValue + def invalidDefaultFloorValue = MIN_DEFAULT_FLOOR_VALUE - 1 + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] + ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue + ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] + ext.prebid.floors.data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor modelGroup default " + + "must be positive float, but was $invalidDefaultFloorValue "] } def "PBS should not invalidate previously good fetched data when floors provider return invalid data"() { @@ -1132,7 +1340,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse.data @@ -1173,12 +1381,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert bidderRequest.ext?.prebid?.floors?.floorMin == floorMin } - def "PBS should reject entire ruleset when modelWeight from floors provider is invalid"() { - given: "Default BidRequest" + def "PBS should reject fetch when modelWeight from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -1190,28 +1405,113 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { data.modelGroups.last().values = [(rule): floorValue] data.modelGroups.last().modelWeight = modelWeight } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() - then: "Bidder request bidFloor should correspond to rule from valid modelGroup" + then: "Bidder request bidFloor should not be passed" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert bidderRequest.imp[0].bidFloor == floorValue + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" + + " must be in range(1-100), but was $invalidModelWeight") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() where: - invalidModelWeight << [0, -1, 1000000] + invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] } - def "PBS should reject entire ruleset when skipRate from floors provider is invalid"() { - given: "Default BidRequest" + def "PBS should reject fetch when root skipRate from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorRules.priceFloorRules.tap { + data.modelGroups << ModelGroup.modelGroup + data.modelGroups.first().values = [(rule): floorValue + 0.1] + data.modelGroups[0].skipRate = 0 + data.skipRate = 0 + skipRate = invalidSkipRate + data.modelGroups.last().values = [(rule): floorValue] + data.modelGroups.last().skipRate = 0 + } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor root skipRate" + + " must be in range(0-100), but was $invalidSkipRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when data skipRate from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, fetch.url in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -1219,10 +1519,229 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorRules.priceFloorRules.tap { data.modelGroups << ModelGroup.modelGroup data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups.first().skipRate = invalidSkipRate + data.modelGroups[0].skipRate = 0 + data.skipRate = invalidSkipRate + skipRate = 0 data.modelGroups.last().values = [(rule): floorValue] data.modelGroups.last().skipRate = 0 } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor data skipRate" + + " must be in range(0-100), but was $invalidSkipRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when modelGroup skipRate from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, fetch.url in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorRules.priceFloorRules.tap { + data.modelGroups << ModelGroup.modelGroup + data.modelGroups.first().values = [(rule): floorValue + 0.1] + data.modelGroups[0].skipRate = invalidSkipRate + data.skipRate = 0 + skipRate = 0 + data.modelGroups.last().values = [(rule): floorValue] + data.modelGroups.last().skipRate = 0 + } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" + + " must be in range(0-100), but was $invalidSkipRate") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + } + + def "PBS should reject fetch when default floor value from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, fetch.url in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1 + def floorsResponse = PriceFloorRules.priceFloorRules.tap { + data.modelGroups << ModelGroup.modelGroup + data.modelGroups.first().values = [(rule): floorValue + 0.1] + data.modelGroups[0].defaultFloor = invalidDefaultFloor + data.modelGroups.last().values = [(rule): floorValue] + data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup default" + + " must be positive float, but was $invalidDefaultFloor") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + } + + def "PBS should reject fetch when floorMin from floors provider is invalid"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, fetch.url in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Set Floors Provider response" + def invalidFloorMin = MIN_FLOOR_MIN - 1 + def floorsResponse = PriceFloorRules.priceFloorRules.tap { + floorMin = invalidFloorMin + } + floorsProvider.setResponse(accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "PBS processes collected metrics request" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + + then: "Bidder request bidFloor should not be passed" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert !bidderRequest.imp[0].bidFloor + assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR + + and: "#FETCH_FAILURE_METRIC should be update" + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, basicFetchUrl) + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + + "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor floorMin" + + " must be positive float, but was $invalidFloorMin") + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + } + + def "PBS should give preference to currency from modelGroups when signalling"() { + given: "Default BidRequest with floors" + def bidRequest = bidRequestWithFloors + + and: "Account with enabled fetch, fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorRules.priceFloorRules.tap { + floorMin = floorValue + data.modelGroups[0].values = [(rule): floorValue] + data.modelGroups[0].currency = modelGroupCurrency + data.currency = dataCurrency + } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) and: "PBS fetch rules from floors provider" @@ -1231,12 +1750,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { when: "PBS processes auction request" floorsPbsService.sendAuctionRequest(bidRequest) - then: "Bidder request bidFloor should correspond to rule from valid modelGroup" + then: "Bidder request should contain bidFloorCur from floors provider according to priority" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert bidderRequest.imp[0].bidFloor == floorValue + assert bidderRequest.imp[0].bidFloorCur == JPY where: - invalidSkipRate << [-1, 101] + modelGroupCurrency | dataCurrency + JPY | EUR + null | JPY } static int convertKilobyteSizeToByte(int kilobyteSize) { 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 5436daa9245..8a83875ed3d 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 @@ -32,11 +32,13 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def "PBS should skip signalling for request with rules when ext.prebid.floors.enabled = false in request"() { given: "Default BidRequest with disabled floors" def bidRequest = bidRequestWithFloors.tap { - ext.prebid.floors.enabled = false + ext.prebid.floors.enabled = requestEnabled } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.enabled = accountEnabled + } accountDao.save(account) when: "PBS processes auction request" @@ -45,10 +47,15 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { then: "Bidder request bidFloor should correspond request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor - assert bidderRequest.ext?.prebid?.floors?.enabled == bidRequest.ext.prebid.floors.enabled + assert !bidderRequest.ext?.prebid?.floors?.enabled and: "PBS should not fetch rules from floors provider" assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0 + + where: + requestEnabled | accountEnabled + false | true + true | false } def "PBS should skip signalling for request without rules when ext.prebid.floors.enabled = false in request"() { @@ -188,6 +195,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue assert bidderRequest.imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + assert !bidderRequest.ext?.prebid?.floors?.skipped where: skipRate << [0, null] @@ -209,7 +217,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorRules.priceFloorRules.tap { data.modelGroups[0].values = [(rule): floorValue] data.modelGroups[0].currency = USD - data.modelGroups[0].skipRate = 100 + data.modelGroups[0].skipRate = modelGroupSkipRate + data.skipRate = dataSkipRate + skipRate = rootSkipRate } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -223,9 +233,17 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor assert bidderRequest.imp[0].bidFloorCur == bidRequest.imp[0].bidFloorCur + assert bidderRequest.ext?.prebid?.floors?.skipRate == 100 + assert bidderRequest.ext?.prebid?.floors?.skipped and: "PBS should not made signalling" assert !bidderRequest.imp[0].ext?.prebid?.floors + + where: + modelGroupSkipRate | dataSkipRate | rootSkipRate + 100 | 0 | 0 + null | 100 | 0 + null | null | 100 } def "PBS should not emit error when request has more rules than fetch.max-rules"() { 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 b76a2b3214e..9eb4da71b24 100644 --- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java @@ -32,6 +32,7 @@ import io.vertx.core.http.HttpMethod; import lombok.AllArgsConstructor; import lombok.Value; +import org.assertj.core.groups.Tuple; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -559,7 +560,7 @@ public void makeHttpRequestsShouldNotSetBidFloorCurrencyToUSDIfNull() { .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).doesNotContainNull() .extracting(Imp::getBidfloor, Imp::getBidfloorcur) - .containsOnly(tuple(BigDecimal.ONE, null)); + .containsOnly(tuple(BigDecimal.ONE, "USD")); } @Test @@ -2643,8 +2644,9 @@ public void makeHttpRequestsShouldReturnOnlyLineItemRequestsWithExpectedFieldsWh @Test public void makeHttpRequestsShouldFillImpExtWithFloors() { // given - final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "USD"); - + final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "JPY"); + when(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .thenReturn(BigDecimal.ONE); when(priceFloorResolver.resolve(any(), any(), any(), any(), any(), any())).thenReturn(priceFloorResult); final BidRequest bidRequest = givenBidRequest( @@ -2663,6 +2665,43 @@ public void makeHttpRequestsShouldFillImpExtWithFloors() { // then assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1).doesNotContainNull() + .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) + .flatExtracting(BidRequest::getImp).doesNotContainNull() + .extracting(Imp::getExt).doesNotContainNull() + .extracting(ext -> mapper.treeToValue(ext, RubiconImpExt.class)) + .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(4001, + mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))), + RubiconImpExtRpTrack.of("", "")), null, 1, null, + RubiconImpExtPrebid.of(ExtImpPrebidFloors.of("video", BigDecimal.ONE, BigDecimal.ONE)))); + } + + @Test + public void makeHttpRequestsShouldAssumeDefaultIpfCurrencyAsUSD() { + // given + final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, null); + when(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .thenReturn(BigDecimal.ONE); + when(priceFloorResolver.resolve(any(), any(), any(), any(), any(), any())).thenReturn(priceFloorResult); + + final BidRequest bidRequest = givenBidRequest( + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .debug(1) + .floors(givenFloors(floors -> floors.data(givenFloorData( + floorData -> floorData.modelGroups(singletonList( + givenModelGroup(UnaryOperator.identity()))))))) + .build())), + builder -> builder.id("123").video(Video.builder().build()), + builder -> builder + .zoneId(4001) + .inventory(mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))))); + + // when + final Result>> result = rubiconBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).containsExactly( + BidderError.badInput("Ipf for imp `123` provided floor with no currency, assuming USD")); assertThat(result.getValue()).hasSize(1).doesNotContainNull() .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) .flatExtracting(BidRequest::getImp).doesNotContainNull() @@ -2714,9 +2753,10 @@ public void makeHttpRequestsShouldConvertBidFloor() { @Test public void makeHttpRequestsShouldFillImpExtWithFloorsWhenBothVideoAndBanner() { // given - final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "USD"); - + final PriceFloorResult priceFloorResult = PriceFloorResult.of("video", BigDecimal.TEN, BigDecimal.TEN, "JPY"); when(priceFloorResolver.resolve(any(), any(), any(), any(), any(), any())).thenReturn(priceFloorResult); + when(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .thenReturn(BigDecimal.ONE); final BidRequest bidRequest = givenBidRequest( builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() @@ -2752,7 +2792,7 @@ public void makeHttpRequestsShouldFillImpExtWithFloorsWhenBothVideoAndBanner() { .containsOnly(RubiconImpExt.of(RubiconImpExtRp.of(4001, mapper.valueToTree(Inventory.of(singletonList("5-star"), singletonList("tech"))), RubiconImpExtRpTrack.of("", "")), null, 1, null, - RubiconImpExtPrebid.of(ExtImpPrebidFloors.of("video", BigDecimal.TEN, BigDecimal.TEN)))); + RubiconImpExtPrebid.of(ExtImpPrebidFloors.of("video", BigDecimal.ONE, BigDecimal.ONE)))); } @Test @@ -2944,7 +2984,8 @@ public void makeBidsShouldReturnBannerBidIfRequestImpHasNoVideo() throws JsonPro // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), banner, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), banner)); } @Test @@ -2963,7 +3004,8 @@ public void makeBidsShouldReturnBannerBidIfRequestImpHasBannerAndVideoButNoRequi // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), banner, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), banner)); } @Test @@ -2983,7 +3025,8 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasBannerAndVideoButAllRequi // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), video, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), video)); } @Test @@ -2999,7 +3042,8 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().price(ONE).build(), video, "USD")); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().price(ONE).build(), video)); } @Test @@ -3116,7 +3160,8 @@ public void makeBidsShouldReturnBidWithBidIdFieldFromBidResponseIfZero() throws // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().id("bidid1").price(ONE).build(), banner, null)); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().id("bidid1").price(ONE).build(), banner)); } @Test @@ -3136,7 +3181,8 @@ public void makeBidsShouldReturnBidWithOriginalBidIdFieldFromBidResponseIfNotZer // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsOnly(BidderBid.of(Bid.builder().id("non-zero").price(ONE).build(), banner, null)); + .extracting(BidderBid::getBid, BidderBid::getType) + .containsOnly(Tuple.tuple(Bid.builder().id("non-zero").price(ONE).build(), banner)); } @Test diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java index e4e6f1d4de8..e46e215b411 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java @@ -297,7 +297,7 @@ public void shouldRejectBidsHavingPriceBelowFloor() { singletonList(BidderBid.of( Bid.builder().id("bidId2").impid("impId").price(BigDecimal.TEN).build(), null, null)), singletonList(BidderError.of("Bid with id 'bidId1' was rejected by floor enforcement: " - + "price 0 is below the floor 1", BidderError.Type.generic))); + + "price 0 is below the floor 1", BidderError.Type.rejected_ipf))); } @Test diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index dac9ca871b3..1dea82d2885 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -6,6 +6,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -36,7 +37,6 @@ 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.same; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -65,7 +65,7 @@ public void setUp() { } @Test - public void shouldDoNothingIfPriceFloorsDisabledForAccount() { + public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForAccount() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(floorsConfig -> floorsConfig.enabled(false)), @@ -79,11 +79,13 @@ public void shouldDoNothingIfPriceFloorsDisabledForAccount() { // then verifyNoInteractions(priceFloorFetcher); - assertThat(result).isSameAs(auctionContext); + assertThat(result) + .isEqualTo(auctionContext.with(givenBidRequest( + identity(), PriceFloorRules.builder().enabled(false).build()))); } @Test - public void shouldDoNothingIfPriceFloorsDisabledForRequest() { + public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForRequest() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(identity()), @@ -97,7 +99,13 @@ public void shouldDoNothingIfPriceFloorsDisabledForRequest() { // then verifyNoInteractions(priceFloorFetcher); - assertThat(result).isSameAs(auctionContext); + assertThat(result) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getFloors) + .extracting(PriceFloorRules::getEnabled) + .isEqualTo(false); } @Test @@ -118,13 +126,14 @@ public void shouldUseFloorsFromProviderIfPresent() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); } @Test - public void shouldNUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { + public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)), @@ -141,6 +150,7 @@ public void shouldNUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); @@ -164,6 +174,7 @@ public void shouldNUseFloorsFromProviderIfUseDynamicDataIsTrue() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); @@ -186,9 +197,11 @@ public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() { // then assertThat(extractFloors(result)) - .isEqualTo(givenFloors(floors -> floors - .fetchStatus(FetchStatus.success) - .location(PriceFloorLocation.noData))); + .extracting(PriceFloorRules::getFetchStatus) + .isEqualTo(FetchStatus.success); + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); } @Test @@ -236,6 +249,7 @@ public void shouldUseFloorsFromRequestIfProviderFloorsMissing() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .floorMin(BigDecimal.ONE) .location(PriceFloorLocation.request))); } @@ -256,8 +270,11 @@ public void shouldTolerateMissingRequestAndProviderFloors() { // then assertThat(extractFloors(result)) - .isEqualTo(givenFloors(floors -> floors - .location(PriceFloorLocation.noData))); + .extracting(PriceFloorRules::getEnabled) + .isEqualTo(true); + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getLocation) + .isEqualTo(PriceFloorLocation.noData); } @Test @@ -275,6 +292,7 @@ public void shouldNotSkipFloorsIfRootSkipRateIsOff() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors + .enabled(true) .skipRate(0) .location(PriceFloorLocation.request))); } @@ -295,6 +313,7 @@ public void shouldSkipFloorsIfRootSkipRateIsOn() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .skipRate(100) + .enabled(false) .skipped(true) .location(PriceFloorLocation.request))); } @@ -316,7 +335,8 @@ public void shouldSkipFloorsIfDataSkipRateIsOn() { // then assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors - .skipRate(0) + .enabled(false) + .skipRate(100) .data(priceFloorData) .skipped(true) .location(PriceFloorLocation.request))); @@ -342,6 +362,8 @@ public void shouldSkipFloorsIfModelGroupSkipRateIsOn() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .data(priceFloorData) + .skipRate(100) + .enabled(false) .skipped(true) .location(PriceFloorLocation.request))); } @@ -385,7 +407,40 @@ public void shouldUseSelectedModelGroup() { priceFloorProcessor.enrichWithPriceFloors(auctionContext); // then - verify(floorResolver).resolve(any(), same(modelGroup), any(), any()); + final ArgumentCaptor captor = ArgumentCaptor.forClass(PriceFloorRules.class); + verify(floorResolver).resolve(any(), captor.capture(), any(), any()); + assertThat(captor.getValue()) + .extracting(PriceFloorRules::getData) + .extracting(PriceFloorData::getModelGroups) + .isEqualTo(singletonList(modelGroup)); + } + + @Test + public void shouldCopyFloorProviderValueFromDataLevel() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .floorMin(BigDecimal.ONE)))); + + final PriceFloorRules providerFloors = givenFloors(floors -> floors + .data(PriceFloorData.builder().floorProvider("someProvider").build()) + .floorMin(BigDecimal.ZERO)); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getData) + .extracting(PriceFloorData::getFloorProvider) + .isEqualTo("someProvider"); + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorProvider) + .isEqualTo("someProvider"); } @Test @@ -510,19 +565,30 @@ private static Imp givenImp(UnaryOperator impCustomizer) { private static PriceFloorRules givenFloors( UnaryOperator floorsCustomizer) { - return floorsCustomizer.apply(PriceFloorRules.builder()).build(); + return floorsCustomizer.apply(PriceFloorRules.builder() + .data(PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE) + .build())) + .build()) + ).build(); } private static PriceFloorData givenFloorData( UnaryOperator floorDataCustomizer) { - return floorDataCustomizer.apply(PriceFloorData.builder()).build(); + return floorDataCustomizer.apply(PriceFloorData.builder() + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE) + .build()))).build(); } private static PriceFloorModelGroup givenModelGroup( UnaryOperator modelGroupCustomizer) { - return modelGroupCustomizer.apply(PriceFloorModelGroup.builder()).build(); + return modelGroupCustomizer.apply(PriceFloorModelGroup.builder() + .value("someKey", BigDecimal.ONE)) + .build(); } private static PriceFloorRules extractFloors(AuctionContext auctionContext) { diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java index 80b89cf5d4f..c456dfabf51 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorResolverTest.java @@ -21,6 +21,7 @@ import org.prebid.server.VertxTest; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorField; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; @@ -83,7 +84,7 @@ public void resolveShouldReturnNullWhenNoModelGroupSchema() { final BidRequest bidRequest = BidRequest.builder().build(); // when and then - assertThat(priceFloorResolver.resolve(bidRequest, PriceFloorModelGroup.builder().build(), + assertThat(priceFloorResolver.resolve(bidRequest, givenRules(PriceFloorModelGroup.builder().build()), Imp.builder().build(), null)).isNull(); } @@ -94,9 +95,9 @@ public void resolveShouldReturnNullWhenModelGroupSchemaFieldsIsEmpty() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", emptyList())) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -107,9 +108,9 @@ public void resolveShouldReturnNullWhenNoModelGroupSchemaFields() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", null)) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -120,10 +121,10 @@ public void resolveShouldReturnNullWhenModelGroupValuesIsEmpty() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) .values(emptyMap()) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -134,9 +135,9 @@ public void resolveShouldReturnNullWhenNoModelGroupValues() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) - .build(), + .build()), Imp.builder().build(), null)).isNull(); } @@ -147,10 +148,10 @@ public void resolveShouldReturnNullWhenNoSiteDomainPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.siteDomain))) .value("siteDomain", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -163,10 +164,10 @@ public void resolveShouldReturnPriceFloorForSiteDomainPresentedBySite() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.siteDomain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -179,10 +180,10 @@ public void resolveShouldReturnPriceFloorForSiteDomainPresentedByApp() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.siteDomain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -193,10 +194,10 @@ public void resolveShouldReturnNullWhenNoPubDomainPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("pubDomain", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -211,10 +212,10 @@ public void resolveShouldReturnPriceFloorForPubDomainPresentedBySite() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -229,10 +230,10 @@ public void resolveShouldReturnPriceFloorForPubDomainPresentedByApp() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -243,10 +244,10 @@ public void resolveShouldReturnNullWhenNoDomainPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("pubDomain", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -259,10 +260,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedBySite() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -275,10 +276,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedByApp() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -293,10 +294,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedBySitePublisher() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("siteDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -311,10 +312,10 @@ public void resolveShouldReturnPriceFloorForDomainPresentedByAppPublisher() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.domain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -325,10 +326,10 @@ public void resolveShouldReturnNullWhenNoBundlePresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.bundle))) .value("bundle", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -343,10 +344,10 @@ public void resolveShouldReturnPriceFloorIfBundlePresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.bundle))) .value("someBundle", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -357,10 +358,10 @@ public void resolveShouldReturnNullWhenNoChannelPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) .value("channel", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -378,10 +379,10 @@ public void resolveShouldReturnPriceFloorIfChannelPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.channel))) .value("someChannelName", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -392,10 +393,10 @@ public void resolveShouldReturnNullWhenMediaTypeDoesNotMatchRuleMediaType() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -406,12 +407,12 @@ public void resolveShouldReturnPriceFloorForCatchAllImpMediaTypeWhenImpContainsM // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("banner", BigDecimal.ONE) .value("*", BigDecimal.TEN) .value("video", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder .video(Video.builder().build())), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); @@ -424,10 +425,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsBannerAndRuleMediaTyp // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("banner", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -439,10 +440,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsVideoInStreamAndRuleM // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder().placement(1).build())), @@ -458,10 +459,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsVideoByEmptyPlacement // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder().placement(null).build())), null @@ -476,10 +477,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsVideoInStreamAndRuleM // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("video-instream", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder().placement(1).build())), null).getFloorValue()) @@ -493,10 +494,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsNativeAndRuleMediaTyp // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("native", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .xNative(Native.builder().build())), null).getFloorValue()) @@ -510,10 +511,10 @@ public void resolveShouldReturnPriceFloorWhenImpMediaTypeIsAudioAndRuleMediaType // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) .value("native", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .xNative(Native.builder().build())), null).getFloorValue()) @@ -527,10 +528,10 @@ public void resolveShouldReturnNullWhenSizeDoesNotMatchRuleSize() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -541,11 +542,11 @@ public void resolveShouldReturnPriceFloorWhenMediaTypeIsBannerAndTakePriorityFor // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("100x150", BigDecimal.ONE) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(Banner.builder() .w(100) @@ -562,12 +563,12 @@ public void resolveShouldReturnPriceFloorForCatchAllWildcardWhenMultipleFormats( // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("400x500", BigDecimal.ONE) .value("*", BigDecimal.TEN) .value("250x300", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(Banner.builder() .w(100) @@ -585,10 +586,10 @@ public void resolveShouldReturnPriceFloorWhenMediaTypeIsBannerAndTakeSizesForFor // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(Banner.builder() .w(250) @@ -604,10 +605,10 @@ public void resolveShouldReturnPriceFloorWhenMediaTypeIsVideoAndTakeSizesForForm // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.size))) .value("250x300", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder .banner(null) .video(Video.builder() @@ -625,10 +626,10 @@ public void resolveShouldReturnNullWhenGptSlotDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("someGptSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -646,10 +647,10 @@ public void resolveShouldReturnPriceFloorIfAdserverNameIsGamAndAdSlotMatchesRule // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("someGptSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -668,10 +669,10 @@ public void resolveShouldReturnNullIfAdserverNameIsNotGamAndAdSlotMatchesRule() // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("someGptSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null)) .isNull(); } @@ -687,10 +688,10 @@ public void resolveShouldReturnPriceFloorFromPbAdSlotIfAdserverNameIsNotGamAndAd // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.gptSlot))) .value("somePbAdSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -702,10 +703,10 @@ public void resolveShouldReturnNullWhenPbAdSlotDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pbAdSlot))) .value("somePbAdSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -720,10 +721,10 @@ public void resolveShouldReturnPriceFloorIfPbAdSlotMatchesRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pbAdSlot))) .value("somePbAdSlot", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.ext(impExt)), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -735,10 +736,10 @@ public void resolveShouldReturnNullWhenCountryDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.country))) .value("USA", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -751,10 +752,10 @@ public void resolveShouldReturnPriceFloorWhenCountryMatchesRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.country))) .value("usa", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -769,10 +770,10 @@ public void resolveShouldReturnPriceFloorWhenCountryAlpha3IsForMatchingRuleAlpha // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.country))) .value("usa", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -784,10 +785,10 @@ public void resolveShouldReturnNullWhenDeviceTypeDoesNotMatchRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("desktop", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null)).isNull(); } @@ -800,10 +801,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesPhone() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -817,10 +818,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesIPhone() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -834,10 +835,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesAndroidMobile( // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -851,10 +852,10 @@ public void resolveShouldReturnPriceFloorForPhoneTypeWhenUaMatchesMobileAndroid( // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("phone", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -868,10 +869,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesTablet() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -885,10 +886,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesIPad() { // when and then final BigDecimal floorValue = priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue(); assertThat(floorValue) .isEqualTo(BigDecimal.TEN); @@ -903,10 +904,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesWindowsNt() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -920,10 +921,10 @@ public void resolveShouldReturnPriceFloorForTabletTypeWhenUaMatchesAndroid() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("tablet", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -937,10 +938,10 @@ public void resolveShouldReturnPriceFloorForDesktopTypeWhenUaDoesNotMatchTabletO // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.deviceType))) .value("desktop", BigDecimal.TEN) - .build(), + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -954,14 +955,14 @@ public void resolveShouldReturnFloorWhenAllFieldsMatchExactly() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|*|300x250", BigDecimal.ONE) .value("*|banner|300x250", BigDecimal.ONE) .value("desktop|banner|300x250", BigDecimal.TEN) .value("desktop|banner|*", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -978,13 +979,13 @@ public void resolveShouldReturnFloorWhenMostAccurateWildcardMatchIsPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|*|300x250", BigDecimal.ONE) .value("desktop|banner|*", BigDecimal.TEN) .value("*|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1001,12 +1002,12 @@ public void resolveShouldReturnFloorWhenNarrowerWildcardMatchIsPresent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|*|*", BigDecimal.ONE) .value("*|banner|300x250", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1023,11 +1024,11 @@ public void resolveShouldReturnFloorWhenCaseIsDifferent() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("deSKtop|baNNer|300X250", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1044,11 +1045,11 @@ public void resolveShouldReturnFloorWhenDelimiterIsNullAndDefaultAssumed() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of(null, List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1065,12 +1066,12 @@ public void resolveShouldReturnDefaultWhenNoMatchingRule() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of(null, List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("video|banner|300x250", BigDecimal.ONE) .defaultFloor(BigDecimal.TEN) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1089,12 +1090,12 @@ public void resolveShouldReturnFloorInRulesCurrency() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .currency("EUR") .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1110,21 +1111,21 @@ public void resolveShouldReturnFloorInRulesCurrencyIfConversionIsNotPossible() { final BidRequest bidRequest = BidRequest.builder() .device(Device.builder().ua("potential desktop type").build()) .ext(ExtRequest.of(ExtRequestPrebid.builder() - .floors(PriceFloorRules.builder() - .floorMin(BigDecimal.ONE) - .floorMinCur("UNKNOWN") - .build()) + .floors(PriceFloorRules.builder() + .floorMin(BigDecimal.ONE) + .floorMinCur("UNKNOWN") + .build()) .build())) .build(); // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .currency("EUR") .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1142,11 +1143,11 @@ public void resolveShouldReturnFloorRuleThatWasSelected() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", List.of(PriceFloorField.deviceType, PriceFloorField.mediaType, PriceFloorField.size))) .value("desktop|banner|300x250", BigDecimal.ONE) - .build(), + .build()), givenImp(impBuilder -> impBuilder.banner(Banner.builder() .w(300) .h(250) @@ -1171,10 +1172,10 @@ public void resolveShouldReturnEffectiveFloorMinIfCurrencyIsTheSameAndAllFloorsR // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.TEN) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } @@ -1198,11 +1199,11 @@ public void resolveShouldReturnConvertedFloorMinInProvidedCurrencyAndFloorMinMor // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .currency("GUF") .value("appDomain", BigDecimal.valueOf(5)) - .build(), givenImp(identity()), null)) + .build()), givenImp(identity()), null)) .isEqualTo(PriceFloorResult.of("appdomain", BigDecimal.valueOf(5), BigDecimal.TEN, "GUF")); } @@ -1223,10 +1224,10 @@ public void resolveShouldReturnCorrectValueAfterRoundingUpFifthDecimalNumber() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.ZERO) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.valueOf(9.0001D)); } @@ -1247,13 +1248,22 @@ public void resolveShouldReturnCorrectValueAfterRoundingUpToWhole() { // when and then assertThat(priceFloorResolver.resolve(bidRequest, - PriceFloorModelGroup.builder() + givenRules(PriceFloorModelGroup.builder() .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.pubDomain))) .value("appDomain", BigDecimal.ZERO) - .build(), givenImp(identity()), null).getFloorValue()) + .build()), givenImp(identity()), null).getFloorValue()) .isEqualTo(BigDecimal.TEN); } + private static PriceFloorRules givenRules(PriceFloorModelGroup modelGroup) { + + return PriceFloorRules.builder() + .data(PriceFloorData.builder() + .modelGroups(singletonList(modelGroup)) + .build()) + .build(); + } + private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id("123") From 3a971695d275a57b06660f0ce7ea9641db123c4d Mon Sep 17 00:00:00 2001 From: mtuchkova <47950518+mtuchkova@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:10:16 +0200 Subject: [PATCH 03/12] Tests: Validating request rules: modelGroups, values (#1827) --- .../server/functional/tests/BaseSpec.groovy | 4 +- .../pricefloors/PriceFloorsBaseSpec.groovy | 6 +- .../PriceFloorsCurrencySpec.groovy | 12 ++-- .../PriceFloorsFetchingSpec.groovy | 65 ++++++++++++++++--- .../PriceFloorsSignalingSpec.groovy | 2 +- 5 files changed, 72 insertions(+), 17 deletions(-) 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 f80a88d7183..239e6a690e3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -54,8 +54,8 @@ abstract class BaseSpec extends Specification { PBSUtils.getRandomNumber(MIN_TIMEOUT, MAX_TIMEOUT) } - protected static Number getCurrentMetricValue(String name) { - def response = defaultPbsService.sendCollectedMetricsRequest() + protected static Number getCurrentMetricValue(PrebidServerService pbsService = defaultPbsService, String name) { + def response = pbsService.sendCollectedMetricsRequest() response[name] ?: 0 } 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 cedcffc37d4..14d5058a688 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 @@ -26,6 +26,7 @@ import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS @PBSTest abstract class PriceFloorsBaseSpec extends BaseSpec { @@ -113,8 +114,9 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { } protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, BidRequest bidRequest) { - pbsService.sendAuctionRequest(bidRequest) - Thread.sleep(1000) + PBSUtils.waitUntil({ pbsService.sendAuctionRequest(bidRequest).ext?.debug?.resolvedRequest?.ext?.prebid?.floors?.fetchStatus != INPROGRESS }, + 5000, + 1000) } protected void cacheFloorsProviderRules(PrebidServerService pbsService = floorsPbsService, 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 e9e4a52165e..d82b329fa88 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 @@ -169,6 +169,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(pbsService, bidRequest) + and: "Flush metrics" + flushMetrics(pbsService) + when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -185,8 +188,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { "to convert from currency $requestFloorCur to desired ad server currency $floorsProviderCur" as String] and: "Metric #GENERAL_ERROR_METRIC should be update" - def metrics = pbsService.sendCollectedMetricsRequest() - assert metrics[GENERAL_ERROR_METRIC] == 1 + assert getCurrentMetricValue(pbsService, GENERAL_ERROR_METRIC) == 1 and: "Bidder request should contain bidFloor, bidFloorCur from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -319,6 +321,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) + and: "Flush metrics" + flushMetrics(floorsPbsService) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -329,8 +334,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { "to convert from currency $requestFloorCur to desired ad server currency $floorsProviderCur" as String] and: "Metric #GENERAL_ERROR_METRIC should be update" - def metrics = floorsPbsService.sendCollectedMetricsRequest() - assert metrics[GENERAL_ERROR_METRIC] == 1 + assert getCurrentMetricValue(floorsPbsService, GENERAL_ERROR_METRIC) == 1 and: "Bidder request should contain bidFloor, bidFloorCur from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index a92088e143d..162cdf30d5e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -58,7 +58,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { cacheFloorsProviderRules(pbsService, bidRequest) when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data" assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 @@ -1049,9 +1049,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } accountDao.save(account) - and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) - when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1066,6 +1063,62 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { "must be positive float, but was $invalidFloorMin "] } + def "PBS should validate rules from request when request doesn't contain modelGroups"() { + given: "Default BidRequest without modelGroups" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups = null + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor rules " + + "should contain at least one model group "] + } + + def "PBS should validate rules from request when request doesn't contain values"() { + given: "Default BidRequest without rules" + def floorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + imp[0].bidFloor = floorValue + ext.prebid.floors.data.modelGroups[0].values = null + } + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request bidFloor should correspond to request.imp.bidFloor" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() + assert bidderRequest.imp[0].bidFloor == floorValue + + and: "Response should contain error" + assert response.ext?.errors[PREBID]*.code == [999] + assert response.ext?.errors[PREBID]*.message == + ["Failed to parse price floors from request, with a reason : Price floor rules values " + + "can't be null or empty, but were null "] + } + def "PBS should validate rules from request when modelWeight from request is invalid"() { given: "Default BidRequest with floors" def floorValue = PBSUtils.randomFloorValue @@ -1279,9 +1332,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } accountDao.save(account) - and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) - when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1376,7 +1426,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "Bidder request floorMin should correspond to floorMin from request" - assert bidder.getRequestCount(bidRequest.id) == 2 def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.ext?.prebid?.floors?.floorMin == floorMin } 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 8a83875ed3d..9024b5aa1ac 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 @@ -468,7 +468,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) when: "PBS cache rules and processes auction request" - cacheFloorsProviderRules(bidRequest, floorValue) + cacheFloorsProviderRules(bidRequest) then: "Bidder request should contain 1 modelGroup" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() From fa759dc212bb3bfd45172b92a901650145acb26a Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Thu, 21 Apr 2022 18:14:13 +0300 Subject: [PATCH 04/12] Price Floors: Increase test coverage (#1829) --- .../floors/BasicPriceFloorProcessor.java | 28 ++- .../floors/PriceFloorRulesValidator.java | 4 +- .../floors/BasicPriceFloorProcessorTest.java | 164 ++++++++++++++ .../floors/PriceFloorRulesValidatorTest.java | 200 ++++++++++++++++++ .../floors/PriceFloorsConfigResolverTest.java | 97 ++++++++- .../java/org/prebid/server/it/DealsTest.java | 4 +- .../org/prebid/server/it/IntegrationTest.java | 39 +--- .../org/prebid/server/it/PriceFloorsTest.java | 113 ++++++++++ .../server/util/IntegrationTestsUtil.java | 84 ++++++++ .../floors/floors-test-auction-request-1.json | 67 ++++++ .../floors/floors-test-auction-request-2.json | 43 ++++ .../floors/floors-test-auction-response.json | 36 ++++ .../floors/floors-test-bid-request-1.json | 96 +++++++++ .../floors/floors-test-bid-request-2.json | 96 +++++++++ .../floors/floors-test-bid-response.json | 19 ++ .../it/openrtb2/floors/provided-floors.json | 25 +++ .../prebid/server/it/test-app-settings.yaml | 6 + 17 files changed, 1071 insertions(+), 50 deletions(-) create mode 100644 src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java create mode 100644 src/test/java/org/prebid/server/it/PriceFloorsTest.java create mode 100644 src/test/java/org/prebid/server/util/IntegrationTestsUtil.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index 0c1f0efa2c6..52d333bd641 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -49,7 +49,7 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; private static final int MODEL_WEIGHT_MAX_VALUE = 100; - private static final int MODEL_WEIGHT_MIN_VALUE = 0; + private static final int MODEL_WEIGHT_MIN_VALUE = 1; private final PriceFloorFetcher floorFetcher; private final PriceFloorResolver floorResolver; @@ -168,21 +168,17 @@ private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, ObjectUtil.getIfNotNull(floorsRequestEnforcement, PriceFloorEnforcement::getEnforceRate); final Price floorMinPrice = resolveFloorMinPrice(requestFloors, providerFloors); - if (floorsEnabledByRequest != null || enforceRate != null || floorMinPrice != null) { - final Boolean floorsEnabledByProvider = - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnabled); - final PriceFloorEnforcement floorsProviderEnforcement = - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnforcement); - - return (providerFloors != null ? providerFloors.toBuilder() : PriceFloorRules.builder()) - .floorMinCur(ObjectUtil.getIfNotNull(floorMinPrice, Price::getCurrency)) - .floorMin(ObjectUtil.getIfNotNull(floorMinPrice, Price::getValue)) - .enabled(resolveFloorsEnabled(floorsEnabledByRequest, floorsEnabledByProvider)) - .enforcement(resolveFloorsEnforcement(floorsProviderEnforcement, enforceRate)) - .build(); - } + final Boolean floorsEnabledByProvider = + ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnabled); + final PriceFloorEnforcement floorsProviderEnforcement = + ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnforcement); - return providerFloors; + return (providerFloors != null ? providerFloors.toBuilder() : PriceFloorRules.builder()) + .floorMinCur(ObjectUtil.getIfNotNull(floorMinPrice, Price::getCurrency)) + .floorMin(ObjectUtil.getIfNotNull(floorMinPrice, Price::getValue)) + .enabled(resolveFloorsEnabled(floorsEnabledByRequest, floorsEnabledByProvider)) + .enforcement(resolveFloorsEnforcement(floorsProviderEnforcement, enforceRate)) + .build(); } private Price resolveFloorMinPrice(PriceFloorRules requestFloors, @@ -317,7 +313,7 @@ private static boolean isValidModelGroup(PriceFloorModelGroup modelGroup) { final Integer modelWeight = modelGroup.getModelWeight(); return modelWeight == null - || (modelWeight > MODEL_WEIGHT_MIN_VALUE && modelWeight < MODEL_WEIGHT_MAX_VALUE); + || (modelWeight >= MODEL_WEIGHT_MIN_VALUE && modelWeight <= MODEL_WEIGHT_MAX_VALUE); } private static int resolveModelGroupWeight(PriceFloorModelGroup modelGroup) { diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java index e61d6bd43de..31057257af5 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -50,7 +50,7 @@ public static void validate(PriceFloorRules priceFloorRules, Integer maxRules) { throw new PreBidException("Price floor rules should contain at least one model group"); } - CollectionUtils.emptyIfNull(data.getModelGroups()).stream() + data.getModelGroups().stream() .filter(Objects::nonNull) .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); } @@ -83,7 +83,7 @@ private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer values)); } - if (values.size() > maxRules) { + if (maxRules != null && values.size() > maxRules) { throw new PreBidException(String.format("Price floor rules number %s exceeded its maximum number %s", values.size(), maxRules)); } diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 1dea82d2885..30606df9170 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -30,6 +30,7 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.UnaryOperator; @@ -37,6 +38,8 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -232,6 +235,167 @@ public void shouldMergeProviderWithRequestFloors() { .location(PriceFloorLocation.fetch))); } + @Test + public void shouldReturnProviderFloorsWhenNotEnabledByRequestAndEnforceRateAndFloorPriceAreAbsent() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(floorsConfig -> floorsConfig.enabled(true)), + givenBidRequest( + identity(), + givenFloors(floors -> floors.data(givenFloorData(identity())).enabled(null)))); + + final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(null).floorMinCur(null)); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + final PriceFloorRules expectedResult = providerFloors.toBuilder() + .enabled(true) + .fetchStatus(FetchStatus.success) + .location(PriceFloorLocation.fetch) + .build(); + + assertThat(extractFloors(result)).isEqualTo(expectedResult); + } + + @Test + public void shouldReturnFloorsWithFloorMinAndCurrencyFromRequestWhenPresent() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .enabled(true) + .floorMin(BigDecimal.ONE) + .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("USD")))))); + + final PriceFloorRules providerFloors = givenFloors(identity()); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) + .containsExactly(BigDecimal.ONE, "USD"); + } + + @Test + public void shouldReturnFloorsWithFloorCurrencyFromRequestAndFloorMinFromProviderWhenRequestFloorMinAbsent() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .enabled(true) + .floorMinCur("USD")))); + + final PriceFloorRules providerFloors = givenFloors(floors -> + floors.floorMin(BigDecimal.ONE) + .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("USD")))); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) + .containsExactly(BigDecimal.ONE, "USD"); + } + + @Test + public void shouldReturnFloorsWithConvertedToRequestCurrencyProviderFloorMin() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .enabled(true) + .floorMinCur("USD")))); + + final PriceFloorRules providerFloors = givenFloors(floors -> + floors.floorMin(BigDecimal.ONE) + .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("UAH")))); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + given(conversionService.convertCurrency( + eq(BigDecimal.ONE), + eq(Collections.emptyMap()), + eq("UAH"), + eq("USD"), + eq(false))) + .willReturn(BigDecimal.valueOf(2)); + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) + .containsExactly(BigDecimal.valueOf(2), "USD"); + } + + @Test + public void shouldReturnFloorsWithConvertedToProviderCurrencyRequestFloorMinFromDefaultUsd() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .enabled(true) + .floorMin(BigDecimal.ONE)))); + + final PriceFloorRules providerFloors = givenFloors(floors -> + floors.floorMin(BigDecimal.valueOf(2)) + .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("UAH")))); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + given(conversionService.convertCurrency( + eq(BigDecimal.ONE), + eq(Collections.emptyMap()), + isNull(), + eq("UAH"), + eq(false))) + .willReturn(BigDecimal.valueOf(2)); + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) + .containsExactly(BigDecimal.valueOf(2), null); + } + + @Test + public void shouldReturnFloorsWithRequestFloorMinCurrencyAndProviderFloorMin() { + // given + final AuctionContext auctionContext = givenAuctionContext( + givenAccount(identity()), + givenBidRequest( + identity(), + givenFloors(floors -> floors + .enabled(true) + .floorMinCur("USD")))); + + final PriceFloorRules providerFloors = givenFloors(floors -> + floors.floorMin(BigDecimal.valueOf(-2)) + .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("UAH")))); + given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + + // when + final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); + + // then + assertThat(extractFloors(result)) + .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) + .containsExactly(BigDecimal.valueOf(-2), "USD"); + } + @Test public void shouldUseFloorsFromRequestIfProviderFloorsMissing() { // given diff --git a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java new file mode 100644 index 00000000000..6c6082c3db4 --- /dev/null +++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java @@ -0,0 +1,200 @@ +package org.prebid.server.floors; + +import org.junit.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorModelGroup; +import org.prebid.server.floors.model.PriceFloorRules; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class PriceFloorRulesValidatorTest extends VertxTest { + + @Test + public void validateShouldThrowExceptionOnInvalidRootSkipRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.skipRate(-1)); + + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor root skipRate must be in range(0-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionWhenFloorMinPresentAndLessThanZero() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRules( + rulesBuilder -> rulesBuilder.floorMin(BigDecimal.valueOf(-1))); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor floorMin must be positive float, but was -1"); + } + + @Test + public void validateShouldThrowExceptionWhenDataIsAbsent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.data(null)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor rules data must be present"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData(dataBuilder -> dataBuilder.skipRate(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor data skipRate must be in range(0-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnAbsentDataModelGroups() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData( + dataBuilder -> dataBuilder.modelGroups(null)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor rules should contain at least one model group"); + } + + @Test + public void validateShouldThrowExceptionOnEmptyDataModelGroups() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData( + dataBuilder -> dataBuilder.modelGroups(Collections.emptyList())); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor rules should contain at least one model group"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataModelGroupModelWeightWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.modelWeight(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor modelGroup modelWeight must be in range(1-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataModelGroupSkipRateWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.skipRate(-1)); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor modelGroup skipRate must be in range(0-100), but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnInvalidDataModelGroupDefaultFloorWhenPresent() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.defaultFloor(BigDecimal.valueOf(-1))); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor modelGroup default must be positive float, but was -1"); + } + + @Test + public void validateShouldThrowExceptionOnEmptyModelGroupValues() { + // given + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + PriceFloorModelGroup.PriceFloorModelGroupBuilder::clearValues); + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .withMessage("Price floor rules values can't be null or empty, but were {}"); + } + + @Test + public void validateShouldThrowExceptionWhenModelGroupValuesSizeGreaterThanMaxRules() { + // given + final Map modelGroupValues = Map.of( + "v1", BigDecimal.TEN, + "v2", BigDecimal.TEN); + + final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups( + modelGroupBuilder -> modelGroupBuilder.clearValues().values(modelGroupValues)); + + final int maxRules = modelGroupValues.size() - 1; + + // when and then + assertThatExceptionOfType(PreBidException.class) + .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, maxRules)) + .withMessage( + "Price floor rules number %s exceeded its maximum number %s", + modelGroupValues.size(), + maxRules); + } + + private static PriceFloorRules givenPriceFloorRulesWithDataModelGroups( + UnaryOperator... modelGroupBuilders) { + + final PriceFloorModelGroup.PriceFloorModelGroupBuilder validModelGroupBuilder = + PriceFloorModelGroup.builder() + .modelWeight(10) + .skipRate(10) + .defaultFloor(BigDecimal.TEN) + .values(Map.of("value", BigDecimal.TEN)); + + final List modelGroups = Arrays.stream(modelGroupBuilders) + .map(modelGroupBuilder -> modelGroupBuilder.apply(validModelGroupBuilder).build()) + .collect(Collectors.toList()); + + return givenPriceFloorRulesWithData(dataBuilder -> dataBuilder.modelGroups(modelGroups)); + } + + private static PriceFloorRules givenPriceFloorRulesWithData( + UnaryOperator dataBuilder) { + + return givenPriceFloorRules(UnaryOperator.identity(), dataBuilder); + } + + private static PriceFloorRules givenPriceFloorRules( + UnaryOperator rulesBuilder) { + + return givenPriceFloorRules(rulesBuilder, UnaryOperator.identity()); + } + + private static PriceFloorRules givenPriceFloorRules( + UnaryOperator rulesBuilder, + UnaryOperator dataBuilder) { + + final PriceFloorRules priceFloorRules = PriceFloorRules.builder() + .skipRate(10) + .floorMin(BigDecimal.TEN) + .data(dataBuilder.apply(PriceFloorData.builder()).build()) + .build(); + + return rulesBuilder.apply(priceFloorRules.toBuilder()).build(); + } +} diff --git a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java index 3490e51d879..450578faf55 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java @@ -19,6 +19,7 @@ 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.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -39,6 +40,13 @@ public void setUp() { metrics); } + @Test + public void priceFloorsConfigResolverShouldNotCreateInstanceIfDefaultAccountIsInvalid() { + assertThatIllegalArgumentException().isThrownBy(() -> new PriceFloorsConfigResolver( + "{", + metrics)); + } + @Test public void updateFloorsConfigShouldNotChangeAccountIfConfigIsValid() { // when @@ -210,6 +218,86 @@ public void updateFloorsConfigShouldReturnDefaultConfigIfMaxFileSizeMoreThanMaxi verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); } + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountEnforceFloorRateIsNotPresent() { + // given + final Account givenAccount = Account.builder() + .id("some-id") + .auction(AccountAuctionConfig.builder() + .priceFloors(AccountPriceFloorsConfig.builder() + .enforceFloorsRate(null).build()) + .build()) + .build(); + + // when + final Future future = testingInstance.updateFloorsConfig(givenAccount); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountMaxFileSizeIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.maxFileSize(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountPeriodicSecIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.periodSec(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountFetchTimeoutIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.timeout(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountMaxRulesIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.maxRules(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + + @Test + public void updateFloorsConfigShouldValidateByDefaultConfigWhenAccountMaxAgeSecIsNotPresent() { + // when + final Future future = testingInstance.updateFloorsConfig( + accountWithFloorsFetchConfig(config -> config.maxAgeSec(null))); + + // then + assertThat(future.result()) + .isEqualTo(withDefaultFloorsConfig(accountBuilder -> accountBuilder.id("some-id"))); + verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors); + } + private static Account accountWithFloorsFetchConfig( UnaryOperator configCustomizer) { return Account.builder() @@ -235,7 +323,14 @@ private static Account withDefaultFloorsConfig(UnaryOperator bidders, Customization... customizations) throws IOException, JSONException { - final List fullCustomizations = new ArrayList<>(Arrays.asList(customizations)); - fullCustomizations.add(new Customization("ext.prebid.auctiontimestamp", (o1, o2) -> true)); - fullCustomizations.add(new Customization("ext.responsetimemillis.cache", (o1, o2) -> true)); - String expectedRequest = replaceStaticInfo(jsonFrom(file)); - for (String bidder : bidders) { - expectedRequest = replaceBidderRelatedStaticInfo(expectedRequest, bidder); - fullCustomizations.add(new Customization( - String.format("ext.responsetimemillis.%s", bidder), (o1, o2) -> true)); - } - JSONAssert.assertEquals(expectedRequest, response.asString(), - new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, - fullCustomizations.toArray(new Customization[0]))); + IntegrationTestsUtil.assertJsonEquals( + file, + response, + bidders, + (json, bidder) -> replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT), + IntegrationTest::replaceStaticInfo, + customizations); } private static String replaceStaticInfo(String json) { - return json.replaceAll("\\{\\{ cache.endpoint }}", CACHE_ENDPOINT) .replaceAll("\\{\\{ cache.resource_url }}", CACHE_ENDPOINT + "?uuid=") .replaceAll("\\{\\{ cache.host }}", HOST_AND_PORT) @@ -271,12 +258,6 @@ private static String replaceStaticInfo(String json) { .replaceAll("\\{\\{ event.url }}", "http://localhost:8080/event?"); } - private static String replaceBidderRelatedStaticInfo(String json, String bidder) { - - return json.replaceAll("\\{\\{ " + bidder + "\\.exchange_uri }}", - "http://" + HOST_AND_PORT + "/" + bidder + "-exchange"); - } - static BidCacheRequestPattern equalToBidCacheRequest(String json) { return new BidCacheRequestPattern(json); } diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java new file mode 100644 index 00000000000..d8ac02e04be --- /dev/null +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -0,0 +1,113 @@ +package org.prebid.server.it; + +import com.github.tomakehurst.wiremock.junit.WireMockClassRule; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.json.JSONException; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.server.VertxTest; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.IntegrationTestsUtil; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import 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.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.util.IntegrationTestsUtil.assertJsonEquals; +import static org.prebid.server.util.IntegrationTestsUtil.jsonFrom; +import static org.prebid.server.util.IntegrationTestsUtil.responseFor; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@RunWith(SpringRunner.class) +@TestPropertySource( + value = {"test-application.properties"}, + properties = {"price-floors.enabled=true", "http.port=55555", "admin.port=0"} +) +public class PriceFloorsTest extends VertxTest { + + private static final int APP_PORT = 55555; + private static final int WIREMOCK_PORT = 8090; + + private static final String PRICE_FLOORS = "Price Floors Test"; + private static final String FLOORS_FROM_REQUEST = "Floors from request"; + private static final String FLOORS_FROM_PROVIDER = "Floors from provider"; + + private static final RequestSpecification SPEC = IntegrationTest.spec(APP_PORT); + + @ClassRule + public static final WireMockClassRule WIRE_MOCK_RULE = new WireMockClassRule(options().port(WIREMOCK_PORT)); + + @BeforeClass + public static void setUp() throws IOException { + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/periodic-update")) + .willReturn(aResponse().withBody(jsonFrom("storedrequests/test-periodic-refresh.json")))); + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/currency-rates")) + .willReturn(aResponse().withBody(jsonFrom("currency/latest.json")))); + WIRE_MOCK_RULE.stubFor(get(urlPathEqualTo("/floors-provider")) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/provided-floors.json")))); + } + + @Test + public void openrtb2AuctionShouldApplyPriceFloorsForTheGenericBidder() throws IOException, JSONException { + // given + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) + .inScenario(PRICE_FLOORS) + .whenScenarioStateIs(STARTED) + .withRequestBody(equalToJson(jsonFrom("openrtb2/floors/floors-test-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/floors-test-bid-response.json"))) + .willSetStateTo(FLOORS_FROM_REQUEST)); + + // when + final Response firstResponse = responseFor( + "openrtb2/floors/floors-test-auction-request-1.json", + Endpoint.openrtb2_auction, + SPEC); + + // then + assertJsonEquals( + "openrtb2/floors/floors-test-auction-response.json", + firstResponse, + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); + + // given + final StubMapping stubMapping = WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/generic-exchange")) + .inScenario(PRICE_FLOORS) + .whenScenarioStateIs(FLOORS_FROM_REQUEST) + .withRequestBody(equalToJson(jsonFrom("openrtb2/floors/floors-test-bid-request-2.json"))) + .willReturn(aResponse().withBody(jsonFrom("openrtb2/floors/floors-test-bid-response.json"))) + .willSetStateTo(FLOORS_FROM_PROVIDER)); + + // when + final Response secondResponse = responseFor( + "openrtb2/floors/floors-test-auction-request-2.json", + Endpoint.openrtb2_auction, + SPEC); + + // then + assertThat(stubMapping.getNewScenarioState()).isEqualTo(FLOORS_FROM_PROVIDER); + assertJsonEquals( + "openrtb2/floors/floors-test-auction-response.json", + secondResponse, + singletonList("generic"), + PriceFloorsTest::replaceBidderRelatedStaticInfo); + } + + private static String replaceBidderRelatedStaticInfo(String json, String bidder) { + return IntegrationTestsUtil.replaceBidderRelatedStaticInfo(json, bidder, WIREMOCK_PORT); + } +} diff --git a/src/test/java/org/prebid/server/util/IntegrationTestsUtil.java b/src/test/java/org/prebid/server/util/IntegrationTestsUtil.java new file mode 100644 index 00000000000..5c20b3cb6d1 --- /dev/null +++ b/src/test/java/org/prebid/server/util/IntegrationTestsUtil.java @@ -0,0 +1,84 @@ +package org.prebid.server.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.json.JSONException; +import org.prebid.server.it.IntegrationTest; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.model.Endpoint; +import org.skyscreamer.jsonassert.Customization; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.comparator.CustomComparator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class IntegrationTestsUtil { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + private IntegrationTestsUtil() { + } + + public static void assertJsonEquals(String file, + Response response, + List bidders, + BiFunction bidderStaticInfoUpdater, + Function staticInfoUpdater, + Customization... customizations) throws IOException, JSONException { + + final List fullCustomizations = new ArrayList<>(Arrays.asList(customizations)); + fullCustomizations.add(new Customization("ext.prebid.auctiontimestamp", (o1, o2) -> true)); + fullCustomizations.add(new Customization("ext.responsetimemillis.cache", (o1, o2) -> true)); + + String expectedRequest = staticInfoUpdater.apply(jsonFrom(file)); + for (String bidder : bidders) { + expectedRequest = bidderStaticInfoUpdater.apply(expectedRequest, bidder); + fullCustomizations.add(new Customization( + String.format("ext.responsetimemillis.%s", bidder), (o1, o2) -> true)); + } + + JSONAssert.assertEquals( + expectedRequest, + response.asString(), + new CustomComparator(JSONCompareMode.NON_EXTENSIBLE, fullCustomizations.toArray(new Customization[0]))); + } + + public static void assertJsonEquals(String file, + Response response, + List bidders, + BiFunction bidderStaticInfoUpdater, + Customization... customizations) throws IOException, JSONException { + + assertJsonEquals(file, response, bidders, bidderStaticInfoUpdater, Function.identity(), customizations); + } + + public static String replaceBidderRelatedStaticInfo(String json, String bidder, int wiremockPort) { + return json.replaceAll("\\{\\{ " + bidder + "\\.exchange_uri }}", + "http://localhost:" + wiremockPort + "/" + bidder + "-exchange"); + } + + public static Response responseFor(String file, Endpoint endpoint, RequestSpecification requestSpecification) + throws IOException { + + return RestAssured.given(requestSpecification) + .header("Referer", "http://www.example.com") + .header("X-Forwarded-For", "193.168.244.1") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + .body(jsonFrom(file)) + .post(endpoint.value()); + } + + public static String jsonFrom(String file) throws IOException { + // workaround to clear formatting + return MAPPER.writeValueAsString(MAPPER.readTree(IntegrationTest.class.getResourceAsStream(file))); + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json new file mode 100644 index 00000000000..a19b6560a42 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-1.json @@ -0,0 +1,67 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "USD", + "ext": { + "generic": { + } + } + } + ], + "site": { + "publisher": { + "id": "12001" + } + }, + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "floors": { + "data": { + "modelGroups": [ + { + "currency": "NZD", + "modelWeight": 100, + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + } + }, + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json new file mode 100644 index 00000000000..3c47ca50e00 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-request-2.json @@ -0,0 +1,43 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "USD", + "ext": { + "generic": { + } + } + } + ], + "site": { + "publisher": { + "id": "12001" + } + }, + "tmax": 5000, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "floors": { + "floorMinCur": "NZD" + }, + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json new file mode 100644 index 00000000000..179a08e5cb1 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-auction-response.json @@ -0,0 +1,36 @@ +{ + "id": "request_id", + "seatbid": [ + { + "bid": [ + { + "id": "bid_id", + "impid": "imp_id", + "price": 8.399, + "adid": "2068416", + "cid": "8048", + "crid": "24080", + "ext": { + "prebid": { + "type": "banner" + }, + "origbidcpm": 13, + "origbidcur":"GBP" + } + } + ], + "seat": "generic", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "generic": "{{ generic.response_time_ms }}" + }, + "prebid": { + "auctiontimestamp": 0 + }, + "tmaxrequest": 5000 + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json new file mode 100644 index 00000000000..1cebf4f1ffd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-1.json @@ -0,0 +1,96 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "NZD", + "ext": { + "prebid": { + "floors": { + "floorRule": "www.example.com|banner|320x250", + "floorRuleValue": 2, + "floorValue": 4 + } + }, + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "12001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + }, + "floors": { + "data": { + "modelGroups": [ + { + "currency": "NZD", + "modelWeight": 100, + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + }, + "enabled": true, + "fetchStatus": "inprogress", + "location": "request" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json new file mode 100644 index 00000000000..b8f0bf034a4 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-request-2.json @@ -0,0 +1,96 @@ +{ + "id": "request_id", + "imp": [ + { + "id": "imp_id", + "banner": { + "w": 320, + "h": 250 + }, + "bidfloor": 4, + "bidfloorcur": "NZD", + "ext": { + "prebid": { + "floors": { + "floorRule": "www.example.com|banner|320x250", + "floorRuleValue": 2, + "floorValue": 4 + } + }, + "bidder": {} + } + } + ], + "site": { + "domain": "www.example.com", + "page": "http://www.example.com", + "publisher": { + "id": "12001", + "domain": "example.com" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "ip": "193.168.244.1" + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "regs": { + "ext": { + "gdpr": 0 + } + }, + "ext": { + "prebid": { + "bidadjustmentfactors": { + "mediatypes": { + "banner": { + "generic": 0.5 + } + } + }, + "channel": { + "name": "web" + }, + "pbs": { + "endpoint": "/openrtb2/auction" + }, + "floors": { + "data": { + "modelGroups": [ + { + "currency": "NZD", + "modelWeight": 100, + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + }, + "enabled": true, + "fetchStatus": "success", + "location": "fetch" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json new file mode 100644 index 00000000000..f6559262b7d --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/floors-test-bid-response.json @@ -0,0 +1,19 @@ +{ + "id": "tid", + "cur": "GBP", + "seatbid": [ + { + "bid": [ + { + "crid": "24080", + "adid": "2068416", + "price": 13, + "id": "bid_id", + "impid": "imp_id", + "cid": "8048" + } + ], + "type": "banner" + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json new file mode 100644 index 00000000000..1f96d298b63 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json @@ -0,0 +1,25 @@ +{ + "data": { + "modelGroups": [ + { + "modelWeight": 100, + "currency": "NZD", + "schema": { + "fields": [ + "domain","mediaType", "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 + } + } + ] + } +} diff --git a/src/test/resources/org/prebid/server/it/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml index 7d3c90274ca..ef28a3481be 100644 --- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml +++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml @@ -120,6 +120,12 @@ accounts: hook-sequence: - module-code: sample-it-module hook-impl-code: rejecting-processed-bidder-response + - id: 12001 + auction: + price-floors: + fetch: + url: http://localhost:8090/floors-provider + enabled: true domains: - rubiconproject.com - www.rubiconproject.com From 08cbce62a173993364126d3d630c3d918186c4c8 Mon Sep 17 00:00:00 2001 From: mtuchkova <47950518+mtuchkova@users.noreply.github.com> Date: Mon, 25 Apr 2022 15:05:31 +0200 Subject: [PATCH 05/12] Fix enforce-valid-account config logic (#1836) --- .../EnrichingApplicationSettings.java | 14 +- .../spring/config/SettingsConfiguration.java | 4 +- .../model/config/AccountConfig.groovy | 4 + .../model/request/auction/Imp.groovy | 2 +- .../model/request/auction/Video.groovy | 2 +- .../functional/tests/AccountSpec.groovy | 256 ++++++++++++++++++ .../org/prebid/server/it/PriceFloorsTest.java | 4 +- .../EnrichingApplicationSettingsTest.java | 25 +- 8 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 6647b2ef09f..fc4fb11a9a2 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -17,16 +17,19 @@ public class EnrichingApplicationSettings implements ApplicationSettings { + private final boolean enforceValidAccount; private final ApplicationSettings delegate; private final PriceFloorsConfigResolver priceFloorsConfigResolver; private final JsonMerger jsonMerger; private final Account defaultAccount; - public EnrichingApplicationSettings(String defaultAccountConfig, + public EnrichingApplicationSettings(boolean enforceValidAccount, + String defaultAccountConfig, ApplicationSettings delegate, PriceFloorsConfigResolver priceFloorsConfigResolver, JsonMerger jsonMerger) { + this.enforceValidAccount = enforceValidAccount; this.delegate = Objects.requireNonNull(delegate); this.jsonMerger = Objects.requireNonNull(jsonMerger); this.priceFloorsConfigResolver = Objects.requireNonNull(priceFloorsConfigResolver); @@ -42,10 +45,13 @@ public Future getAccountById(String accountId, Timeout timeout) { if (defaultAccount == null) { return accountFuture; } + final Future mergedWithDefaultAccount = accountFuture + .map(this::mergeAccounts); - return accountFuture - .map(this::mergeAccounts) - .otherwise(mergeAccounts(Account.empty(accountId))); + // In case of invalid account return failed future + return enforceValidAccount + ? mergedWithDefaultAccount + : mergedWithDefaultAccount.otherwise(mergeAccounts(Account.empty(accountId))); } @Override diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java index 72ceea87e01..7ef4ae5fbdd 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -340,12 +340,14 @@ static class EnrichingSettingsConfiguration { @Bean EnrichingApplicationSettings enrichingApplicationSettings( + @Value("${settings.enforce-valid-account}") boolean enforceValidAccount, @Value("${settings.default-account-config:#{null}}") String defaultAccountConfig, CompositeApplicationSettings compositeApplicationSettings, PriceFloorsConfigResolver priceFloorsConfigResolver, JsonMerger jsonMerger) { - return new EnrichingApplicationSettings(defaultAccountConfig, + return new EnrichingApplicationSettings(enforceValidAccount, + defaultAccountConfig, compositeApplicationSettings, priceFloorsConfigResolver, jsonMerger); diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy index f344c203618..3d88cb51248 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountConfig.groovy @@ -18,4 +18,8 @@ class AccountConfig { AccountAnalyticsConfig analytics AccountCookieSyncConfig cookieSync AccountHooksConfiguration hooks + + static getDefaultAccountConfig() { + new AccountConfig(status: AccountStatus.ACTIVE) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy index acae8b3e586..4ce40176d72 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy @@ -44,7 +44,7 @@ class Imp { } } - private static Imp getDefaultImp() { + private static Imp getDefaultImp() { new Imp().tap { id = UUID.randomUUID() ext = ImpExt.defaultImpExt diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy index de67c5b3664..e009b5a1741 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Video.groovy @@ -32,6 +32,6 @@ class Video { List companiontype static Video getDefaultVideo() { - new Video(mimes: ["video/mp4"], w: 300, h: 200) + new Video(mimes: ["video/mp4"], w: 300, h: 200) } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy new file mode 100644 index 00000000000..e679594ce07 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy @@ -0,0 +1,256 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.AccountStatus +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Site +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.util.PBSUtils + +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED + +class AccountSpec extends BaseSpec { + + def "PBS should reject request with inactive account"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account": enforceValidAccount as String]) + + and: "Inactive account id" + def accountId = PBSUtils.randomNumber + def account = new Account(uuid: accountId, config: new AccountConfig(status: AccountStatus.INACTIVE)) + accountDao.save(account) + + and: "Default basic BidRequest with inactive account id" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = accountId + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Account $accountId is inactive" + + where: + enforceValidAccount << [true, false] + } + + def "PBS should reject request with unknown account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Non-existing account id" + def accountId = PBSUtils.randomNumber + + and: "Default basic BidRequest with non-existing account id" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = accountId + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: $accountId" + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } + + def "PBS should reject request without account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default basic BidRequest without account" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = null + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: " + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } + + def "PBS should not reject request with unknown account when settings.enforce-valid-account = false"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "false", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default basic BidRequest with non-existing account id" + def bidRequest = BidRequest.defaultBidRequest.tap { + site.publisher.id = accountId + } + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + defaultAccountConfig || accountId + null || null + null || PBSUtils.randomNumber + AccountConfig.defaultAccountConfig || null + AccountConfig.defaultAccountConfig || PBSUtils.randomNumber + } + + def "PBS AMP should reject request with unknown account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request with non-existing account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = requestAccount + } + + and: "Default stored request with non-existing account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = storedRequestAccount + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + pbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + def resolvedAccount = requestAccount ?: storedRequestAccount + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: $resolvedAccount" + + where: + defaultAccountConfig || requestAccount || storedRequestAccount + null || PBSUtils.randomNumber || null + null || null || PBSUtils.randomNumber + AccountConfig.defaultAccountConfig || PBSUtils.randomNumber || null + AccountConfig.defaultAccountConfig || null || PBSUtils.randomNumber + } + + def "PBS AMP should reject request without account when settings.enforce-valid-account = true"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "true", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request without account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = null + } + + and: "Default stored request without account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = null + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + pbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Unauthorized account id: " + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } + + def "PBS AMP should not reject request with unknown account when settings.enforce-valid-account = false"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "false", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request with non-existing account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = requestAccount + } + + and: "Default stored request with non-existing account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = null + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = pbsService.sendAmpRequest(ampRequest) + + then: "PBS should not reject request" + assert response.targeting + assert response.ext?.debug?.httpcalls + + where: + defaultAccountConfig || requestAccount || storedRequestAccount + null || PBSUtils.randomNumber || null + null || null || PBSUtils.randomNumber + AccountConfig.defaultAccountConfig || PBSUtils.randomNumber || null + AccountConfig.defaultAccountConfig || null || PBSUtils.randomNumber + } + + def "PBS AMP should not reject request without account when settings.enforce-valid-account = false"() { + given: "Pbs config with enforce-valid-account and default-account-config" + def pbsService = pbsServiceFactory.getService( + ["settings.enforce-valid-account" : "false", + "settings.default-account-config": mapper.encode(defaultAccountConfig)]) + + and: "Default AMP request without account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + account = null + } + + and: "Default stored request without account" + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + site = Site.defaultSite + site.publisher.id = null + } + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = pbsService.sendAmpRequest(ampRequest) + + then: "PBS should not reject request" + assert response.targeting + assert response.ext?.debug?.httpcalls + + where: + defaultAccountConfig << [null, AccountConfig.defaultAccountConfig] + } +} diff --git a/src/test/java/org/prebid/server/it/PriceFloorsTest.java b/src/test/java/org/prebid/server/it/PriceFloorsTest.java index d8ac02e04be..8d6620d0869 100644 --- a/src/test/java/org/prebid/server/it/PriceFloorsTest.java +++ b/src/test/java/org/prebid/server/it/PriceFloorsTest.java @@ -35,11 +35,11 @@ @RunWith(SpringRunner.class) @TestPropertySource( value = {"test-application.properties"}, - properties = {"price-floors.enabled=true", "http.port=55555", "admin.port=0"} + properties = {"price-floors.enabled=true", "http.port=8050", "admin.port=0"} ) public class PriceFloorsTest extends VertxTest { - private static final int APP_PORT = 55555; + private static final int APP_PORT = 8050; private static final int WIREMOCK_PORT = 8090; private static final String PRICE_FLOORS = "Price Floors Test"; diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java index 34aaced63e7..8fe44bad80d 100644 --- a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java +++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java @@ -51,7 +51,7 @@ public void setUp() { public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsNull() { // given enrichingApplicationSettings = - new EnrichingApplicationSettings(null, delegate, priceFloorsConfigResolver, jsonMerger); + new EnrichingApplicationSettings(true, null, delegate, priceFloorsConfigResolver, jsonMerger); final Account returnedAccount = Account.builder().build(); given(delegate.getAccountById(anyString(), any())).willReturn(Future.succeededFuture(returnedAccount)); @@ -70,6 +70,7 @@ public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsNull() { public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsEmpty() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + true, "{}", delegate, priceFloorsConfigResolver, @@ -92,6 +93,7 @@ public void getAccountByIdShouldOmitMergingWhenDefaultAccountIsEmpty() { public void getAccountByIdShouldMergeAccountWithDefaultAccount() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + true, "{\"auction\": {\"banner-cache-ttl\": 100}," + "\"privacy\": {\"gdpr\": {\"enabled\": true, \"channel-enabled\": {\"web\": false}}}}", delegate, @@ -133,6 +135,7 @@ public void getAccountByIdShouldMergeAccountWithDefaultAccount() { public void getAccountByIdShouldReturnDefaultAccountWhenDelegateFailed() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + false, "{\"auction\": {\"banner-cache-ttl\": 100}}", delegate, priceFloorsConfigResolver, @@ -152,10 +155,30 @@ public void getAccountByIdShouldReturnDefaultAccountWhenDelegateFailed() { .build()); } + @Test + public void getAccountByIdShouldReturnFailedFutureWhenDelegateFailedAndEnforceValidAccountIsTrue() { + // given + enrichingApplicationSettings = new EnrichingApplicationSettings( + true, + "{\"auction\": {\"banner-cache-ttl\": 100}}", + delegate, + priceFloorsConfigResolver, + jsonMerger); + + given(delegate.getAccountById(anyString(), any())).willReturn(Future.failedFuture("Exception")); + + // when + final Future accountFuture = enrichingApplicationSettings.getAccountById("123", timeout); + + // then + assertThat(accountFuture).isFailed(); + } + @Test public void getAccountByIdShouldPassOnFailureWhenDefaultAccountIsEmpty() { // given enrichingApplicationSettings = new EnrichingApplicationSettings( + true, "{}", delegate, priceFloorsConfigResolver, From 1a9e1a9feb5b64c523946fc3d213f998050fe679 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Tue, 26 Apr 2022 11:18:54 +0300 Subject: [PATCH 06/12] Prebid Server prepare release 1.87.0 --- extra/bundle/pom.xml | 6 +++--- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pom.xml | 4 ++-- extra/pom.xml | 4 ++-- pom.xml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 6fc7a23a818..5b6e7ce7895 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.87.0 ../../extra/pom.xml @@ -77,12 +77,12 @@ org.prebid prebid-server - 1.87.0-SNAPSHOT + 1.87.0 org.prebid.server.hooks.modules ortb2-blocking - 1.87.0-SNAPSHOT + 1.87.0 diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 45feecb615c..a2f000b5d95 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 - 1.87.0-SNAPSHOT + 1.87.0 ortb2-blocking diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index b95b66f88b3..e794629f910 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.87.0 ../../extra/pom.xml @@ -26,7 +26,7 @@ 11 11 - 1.87.0-SNAPSHOT + 1.87.0 1.18.22 diff --git a/extra/pom.xml b/extra/pom.xml index ee001d5264d..cea5f968345 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.87.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 + 1.87.0 diff --git a/pom.xml b/pom.xml index 47642d84a3e..7e7f30bf1c0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0-SNAPSHOT + 1.87.0 extra/pom.xml From 7cbd80564509e5d7d4f1dcc61cde0ece6dcce60d Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Tue, 26 Apr 2022 12:05:56 +0300 Subject: [PATCH 07/12] Prebid Server prepare for next development iteration (#1838) --- extra/bundle/pom.xml | 6 +++--- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pom.xml | 4 ++-- extra/pom.xml | 4 ++-- pom.xml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 5b6e7ce7895..d15bc241e71 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0 + 1.87.1-SNAPSHOT ../../extra/pom.xml @@ -77,12 +77,12 @@ org.prebid prebid-server - 1.87.0 + 1.87.1-SNAPSHOT org.prebid.server.hooks.modules ortb2-blocking - 1.87.0 + 1.87.1-SNAPSHOT diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index a2f000b5d95..2d550e0a6c0 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 - 1.87.0 + 1.87.1-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index e794629f910..464f8dd4e0f 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0 + 1.87.1-SNAPSHOT ../../extra/pom.xml @@ -26,7 +26,7 @@ 11 11 - 1.87.0 + 1.87.1-SNAPSHOT 1.18.22 diff --git a/extra/pom.xml b/extra/pom.xml index cea5f968345..ee6d518b953 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 1.87.0 + 1.87.1-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 - 1.87.0 + HEAD diff --git a/pom.xml b/pom.xml index 7e7f30bf1c0..fedb5571e77 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.0 + 1.87.1-SNAPSHOT extra/pom.xml From 910757065fce4990f20972f0db3d68d833e919d5 Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Tue, 26 Apr 2022 15:46:59 +0300 Subject: [PATCH 08/12] Price Floors: Provider contract change (#1837) --- .../floors/BasicPriceFloorProcessor.java | 92 +----- .../server/floors/PriceFloorFetcher.java | 54 +-- .../floors/PriceFloorRulesValidator.java | 15 +- .../server/floors/model/PriceFloorRules.java | 14 - .../server/floors/proto/FetchResult.java | 4 +- .../config/PriceFloorsConfiguration.java | 3 +- .../floorsprovider/PriceFloorEndpoint.groovy | 9 - .../floorsprovider/PriceFloorRules.groovy | 26 -- .../model/pricefloors/PriceFloorData.groovy | 3 +- .../scaffolding/FloorsProvider.groovy | 4 +- .../PriceFloorsCurrencySpec.groovy | 47 ++- .../PriceFloorsEnforcementSpec.groovy | 43 ++- .../PriceFloorsFetchingSpec.groovy | 291 +++++------------ .../pricefloors/PriceFloorsRulesSpec.groovy | 125 +++---- .../PriceFloorsSignalingSpec.groovy | 70 ++-- .../floors/BasicPriceFloorProcessorTest.java | 208 ++++-------- .../server/floors/PriceFloorFetcherTest.java | 89 +++-- .../floors/PriceFloorRulesValidatorTest.java | 22 +- .../prebid/server/functional/floor-rules.json | 308 +++++++++--------- .../it/openrtb2/floors/provided-floors.json | 44 +-- 20 files changed, 549 insertions(+), 922 deletions(-) delete mode 100644 src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy delete mode 100644 src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index 52d333bd641..deb987d3cd2 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java @@ -12,10 +12,8 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.model.Price; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorData; -import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorLocation; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorResult; @@ -53,17 +51,14 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { private final PriceFloorFetcher floorFetcher; private final PriceFloorResolver floorResolver; - private final CurrencyConversionService conversionService; private final JacksonMapper mapper; public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher, PriceFloorResolver floorResolver, - CurrencyConversionService conversionService, JacksonMapper mapper) { this.floorFetcher = Objects.requireNonNull(floorFetcher); this.floorResolver = Objects.requireNonNull(floorResolver); - this.conversionService = Objects.requireNonNull(conversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -127,13 +122,13 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus); if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) { - final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRules()); + final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData()); return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch); } if (requestFloors != null) { try { - PriceFloorRulesValidator.validate(requestFloors, Integer.MAX_VALUE); + PriceFloorRulesValidator.validateRules(requestFloors, Integer.MAX_VALUE); return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); } catch (PreBidException e) { errors.add(String.format("Failed to parse price floors from request," @@ -159,30 +154,18 @@ private static boolean shouldUseDynamicData(Account account) { } private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, - PriceFloorRules providerFloors) { + PriceFloorData providerRulesData) { - final Boolean floorsEnabledByRequest = ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getEnabled); - final PriceFloorEnforcement floorsRequestEnforcement = - ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getEnforcement); - final Integer enforceRate = - ObjectUtil.getIfNotNull(floorsRequestEnforcement, PriceFloorEnforcement::getEnforceRate); - final Price floorMinPrice = resolveFloorMinPrice(requestFloors, providerFloors); + final Price floorMinPrice = resolveFloorMinPrice(requestFloors); - final Boolean floorsEnabledByProvider = - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnabled); - final PriceFloorEnforcement floorsProviderEnforcement = - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnforcement); - - return (providerFloors != null ? providerFloors.toBuilder() : PriceFloorRules.builder()) + return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder()) .floorMinCur(ObjectUtil.getIfNotNull(floorMinPrice, Price::getCurrency)) .floorMin(ObjectUtil.getIfNotNull(floorMinPrice, Price::getValue)) - .enabled(resolveFloorsEnabled(floorsEnabledByRequest, floorsEnabledByProvider)) - .enforcement(resolveFloorsEnforcement(floorsProviderEnforcement, enforceRate)) + .data(providerRulesData) .build(); } - private Price resolveFloorMinPrice(PriceFloorRules requestFloors, - PriceFloorRules providerFloors) { + private Price resolveFloorMinPrice(PriceFloorRules requestFloors) { final String requestDataCurrency = ObjectUtil.getIfNotNull( ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getData), PriceFloorData::getCurrency); @@ -191,66 +174,11 @@ private Price resolveFloorMinPrice(PriceFloorRules requestFloors, requestDataCurrency); final BigDecimal requestFloorMin = ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getFloorMin); - final String providerFloorMinCur = - ObjectUtil.getIfNotNull( - ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getData), PriceFloorData::getCurrency); - final BigDecimal providerFloorMin = ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getFloorMin); - - if (StringUtils.isNotBlank(requestFloorMinCur)) { - if (BidderUtil.isValidPrice(requestFloorMin)) { - return Price.of(requestFloorMinCur, requestFloorMin); - } else if (BidderUtil.isValidPrice(providerFloorMin)) { - if (StringUtils.equals(providerFloorMinCur, requestFloorMinCur)) { - return Price.of(requestFloorMinCur, providerFloorMin); - } - - return Price.of( - requestFloorMinCur, - conversionService.convertCurrency( - providerFloorMin, - Collections.emptyMap(), - providerFloorMinCur, - requestFloorMinCur, - false)); - } - } - - if (StringUtils.isNotBlank(providerFloorMinCur)) { - if (BidderUtil.isValidPrice(requestFloorMin)) { - return Price.of( - requestFloorMinCur, - conversionService.convertCurrency( - requestFloorMin, - Collections.emptyMap(), - requestFloorMinCur, - providerFloorMinCur, - false)); - } - - return Price.of(requestFloorMinCur, providerFloorMin); - } - - return Price.of(null, ObjectUtils.firstNonNull(requestFloorMin, providerFloorMin)); - } - - private static Boolean resolveFloorsEnabled(Boolean enabledByRequest, Boolean enabledByProvider) { - if (BooleanUtils.isFalse(enabledByRequest) || BooleanUtils.isFalse(enabledByProvider)) { - return false; + if (StringUtils.isNotBlank(requestFloorMinCur) && BidderUtil.isValidPrice(requestFloorMin)) { + return Price.of(requestFloorMinCur, requestFloorMin); } - return ObjectUtils.defaultIfNull(enabledByRequest, enabledByProvider); - } - - private static PriceFloorEnforcement resolveFloorsEnforcement(PriceFloorEnforcement providerEnforcement, - Integer enforceRate) { - - if (enforceRate == null) { - return providerEnforcement; - } - - return (providerEnforcement != null ? providerEnforcement.toBuilder() : PriceFloorEnforcement.builder()) - .enforceRate(enforceRate) - .build(); + return Price.of(null, requestFloorMin); } private static PriceFloorRules createFloorsFrom(PriceFloorRules floors, diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index e0bb90ae395..ea583544e7e 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java @@ -16,8 +16,8 @@ import org.apache.http.HttpStatus; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorDebugProperties; -import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.json.DecodeException; @@ -85,11 +85,11 @@ public FetchResult fetch(Account account) { final AccountFetchContext accountFetchContext = fetchedData.get(account.getId()); return accountFetchContext != null - ? FetchResult.of(accountFetchContext.getRules(), accountFetchContext.getFetchStatus()) - : fetchPriceFloorRules(account); + ? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus()) + : fetchPriceFloorData(account); } - private FetchResult fetchPriceFloorRules(Account account) { + private FetchResult fetchPriceFloorData(Account account) { final AccountPriceFloorsFetchConfig fetchConfig = getFetchConfig(account); final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled); @@ -104,7 +104,7 @@ private FetchResult fetchPriceFloorRules(Account account) { return FetchResult.of(null, FetchStatus.error); } if (!fetchInProgress.contains(accountId)) { - fetchPriceFloorRulesAsynchronous(fetchConfig, accountId); + fetchPriceFloorDataAsynchronous(fetchConfig, accountId); } return FetchResult.of(null, FetchStatus.inprogress); @@ -131,7 +131,7 @@ private static AccountPriceFloorsFetchConfig getFetchConfig(Account account) { return ObjectUtil.getIfNotNull(priceFloorsConfig, AccountPriceFloorsConfig::getFetch); } - private void fetchPriceFloorRulesAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) { + private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) { final Long accountTimeout = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getTimeout); final Long timeout = ObjectUtils.firstNonNull( ObjectUtil.getIfNotNull(debugProperties, PriceFloorDebugProperties::getMinTimeoutMs), @@ -146,7 +146,7 @@ private void fetchPriceFloorRulesAsynchronous(AccountPriceFloorsFetchConfig fetc .map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId)) .recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId)) .map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId)) - .map(priceFloorRules -> createPeriodicTimerForRulesFetch(priceFloorRules, fetchConfig, accountId)); + .map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId)); } private static long resolveMaxFileSize(Long maxSizeInKBytes) { @@ -169,24 +169,24 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon + "response body can not be empty", accountId)); } - final PriceFloorRules priceFloorRules = parsePriceFloorRules(body, accountId); - PriceFloorRulesValidator.validate(priceFloorRules, resolveMaxRules(fetchConfig.getMaxRules())); + final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId); + PriceFloorRulesValidator.validateRulesData(priceFloorData, resolveMaxRules(fetchConfig.getMaxRules())); - return ResponseCacheInfo.of(priceFloorRules, + return ResponseCacheInfo.of(priceFloorData, FetchStatus.success, cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl())); } - private PriceFloorRules parsePriceFloorRules(String body, String accountId) { - final PriceFloorRules priceFloorRules; + private PriceFloorData parsePriceFloorData(String body, String accountId) { + final PriceFloorData priceFloorData; try { - priceFloorRules = mapper.decodeValue(body, PriceFloorRules.class); + priceFloorData = mapper.decodeValue(body, PriceFloorData.class); } catch (DecodeException e) { throw new PreBidException( String.format("Failed to parse price floor response for account %s, cause: %s", accountId, ExceptionUtils.getMessage(e))); } - return priceFloorRules; + return priceFloorData; } private static int resolveMaxRules(Long accountMaxRules) { @@ -214,20 +214,20 @@ private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String return null; } - private PriceFloorRules updateCache(ResponseCacheInfo cacheInfo, - AccountPriceFloorsFetchConfig fetchConfig, - String accountId) { + private PriceFloorData updateCache(ResponseCacheInfo cacheInfo, + AccountPriceFloorsFetchConfig fetchConfig, + String accountId) { long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig)); final AccountFetchContext fetchContext = - AccountFetchContext.of(cacheInfo.getRules(), cacheInfo.getFetchStatus(), maxAgeTimerId); + AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId); - if (cacheInfo.getRules() != null || !fetchedData.containsKey(accountId)) { + if (cacheInfo.getRulesData() != null || !fetchedData.containsKey(accountId)) { fetchedData.put(accountId, fetchContext); fetchInProgress.remove(accountId); } - return fetchContext.getRules(); + return fetchContext.getRulesData(); } private long resolveCacheTtl(ResponseCacheInfo cacheInfo, AccountPriceFloorsFetchConfig fetchConfig) { @@ -277,9 +277,9 @@ private Future recoverFromFailedFetching(Throwable throwable, return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus)); } - private PriceFloorRules createPeriodicTimerForRulesFetch(PriceFloorRules priceFloorRules, - AccountPriceFloorsFetchConfig fetchConfig, - String accountId) { + private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData, + AccountPriceFloorsFetchConfig fetchConfig, + String accountId) { final long accountPeriodicTimeSec = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec); final long periodicTimeSec = @@ -288,11 +288,11 @@ private PriceFloorRules createPeriodicTimerForRulesFetch(PriceFloorRules priceFl accountPeriodicTimeSec); vertx.setTimer(TimeUnit.SECONDS.toMillis(periodicTimeSec), ignored -> periodicFetch(accountId)); - return priceFloorRules; + return priceFloorData; } private void periodicFetch(String accountId) { - accountById(accountId).map(this::fetchPriceFloorRules); + accountById(accountId).map(this::fetchPriceFloorData); } private Future accountById(String accountId) { @@ -306,7 +306,7 @@ private Future accountById(String accountId) { @Value(staticConstructor = "of") private static class AccountFetchContext { - PriceFloorRules rules; + PriceFloorData rulesData; FetchStatus fetchStatus; @@ -316,7 +316,7 @@ private static class AccountFetchContext { @Value(staticConstructor = "of") private static class ResponseCacheInfo { - PriceFloorRules rules; + PriceFloorData rulesData; FetchStatus fetchStatus; diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java index 31057257af5..086bf9f5919 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -21,7 +21,7 @@ public class PriceFloorRulesValidator { private PriceFloorRulesValidator() { } - public static void validate(PriceFloorRules priceFloorRules, Integer maxRules) { + public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules) { final Integer rootSkipRate = priceFloorRules.getSkipRate(); if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) { @@ -35,22 +35,25 @@ public static void validate(PriceFloorRules priceFloorRules, Integer maxRules) { + "must be positive float, but was %s", floorMin)); } - final PriceFloorData data = priceFloorRules.getData(); - if (data == null) { + validateRulesData(priceFloorRules.getData(), maxRules); + } + + public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules) { + if (priceFloorData == null) { throw new PreBidException("Price floor rules data must be present"); } - final Integer dataSkipRate = data.getSkipRate(); + final Integer dataSkipRate = priceFloorData.getSkipRate(); if (dataSkipRate != null && (dataSkipRate < SKIP_RATE_MIN || dataSkipRate > SKIP_RATE_MAX)) { throw new PreBidException(String.format("Price floor data skipRate " + "must be in range(0-100), but was %s", dataSkipRate)); } - if (CollectionUtils.isEmpty(data.getModelGroups())) { + if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) { throw new PreBidException("Price floor rules should contain at least one model group"); } - data.getModelGroups().stream() + priceFloorData.getModelGroups().stream() .filter(Objects::nonNull) .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); } diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java b/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java index 993de9c39e1..ccc5e05bb5e 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorRules.java @@ -7,22 +7,10 @@ import java.math.BigDecimal; -/** - * This model is a trade-off. - *

- * It defines both: - * 1. The contract for prebid server bidrequest.ext.prebid.floors field. - * 2. The contract for floors provider (assuming prebid server specific fields will not be overridden). - *

- * To make things better, it should be divided in two separate models: - * for prebid request and floors provider. - */ @Value @Builder(toBuilder = true) public class PriceFloorRules { - // prebid server and floors provider fields - @JsonProperty("floorMin") BigDecimal floorMin; @@ -38,8 +26,6 @@ public class PriceFloorRules { PriceFloorData data; - // prebid server specific fields - Boolean enabled; @JsonProperty("fetchStatus") diff --git a/src/main/java/org/prebid/server/floors/proto/FetchResult.java b/src/main/java/org/prebid/server/floors/proto/FetchResult.java index 774f7e23932..36c4fda58e0 100644 --- a/src/main/java/org/prebid/server/floors/proto/FetchResult.java +++ b/src/main/java/org/prebid/server/floors/proto/FetchResult.java @@ -1,12 +1,12 @@ package org.prebid.server.floors.proto; import lombok.Value; -import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.floors.model.PriceFloorData; @Value(staticConstructor = "of") public class FetchResult { - PriceFloorRules rules; + PriceFloorData rulesData; FetchStatus fetchStatus; } 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 4e881101264..20f9a9f81cf 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -79,10 +79,9 @@ PriceFloorResolver noOpPriceFloorResolver() { @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") PriceFloorProcessor basicPriceFloorProcessor(PriceFloorFetcher floorFetcher, PriceFloorResolver floorResolver, - CurrencyConversionService conversionService, JacksonMapper mapper) { - return new BasicPriceFloorProcessor(floorFetcher, floorResolver, conversionService, mapper); + return new BasicPriceFloorProcessor(floorFetcher, floorResolver, mapper); } @Bean diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy deleted file mode 100644 index 6f99c65502a..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorEndpoint.groovy +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.functional.model.mock.services.floorsprovider - -import groovy.transform.ToString - -@ToString(includeNames = true, ignoreNulls = true) -class PriceFloorEndpoint { - - String url -} diff --git a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy b/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy deleted file mode 100644 index ff3af553ded..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/mock/services/floorsprovider/PriceFloorRules.groovy +++ /dev/null @@ -1,26 +0,0 @@ -package org.prebid.server.functional.model.mock.services.floorsprovider - -import groovy.transform.ToString -import org.prebid.server.functional.model.ResponseModel -import org.prebid.server.functional.model.pricefloors.PriceFloorData -import org.prebid.server.functional.model.pricefloors.PriceFloorEnforcement -import org.prebid.server.functional.util.PBSUtils - -import static org.prebid.server.functional.tests.pricefloors.PriceFloorsBaseSpec.FLOOR_MIN - -@ToString(includeNames = true, ignoreNulls = true) -class PriceFloorRules implements ResponseModel { - - BigDecimal floorMin - String floorProvider - PriceFloorEnforcement enforcement - Integer skipRate - PriceFloorEndpoint endpoint - PriceFloorData data - - static PriceFloorRules getPriceFloorRules() { - new PriceFloorRules(floorMin: FLOOR_MIN, - floorProvider: PBSUtils.randomString, - data: PriceFloorData.priceFloorData) - } -} diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy index 4498bb3e30d..b9af0cd1dc7 100644 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy @@ -3,13 +3,14 @@ package org.prebid.server.functional.model.pricefloors import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.ResponseModel import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.USD @EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) -class PriceFloorData { +class PriceFloorData implements ResponseModel { String floorProvider Currency currency diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy index 935a20b80a7..00bbf9c3fe4 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/FloorsProvider.groovy @@ -4,7 +4,7 @@ import org.mockserver.matchers.TimeToLive import org.mockserver.matchers.Times import org.mockserver.model.HttpRequest import org.mockserver.model.HttpResponse -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.util.ObjectMapperWrapper import org.testcontainers.containers.MockServerContainer @@ -39,6 +39,6 @@ class FloorsProvider extends NetworkScaffolding { } private String getDefaultResponse() { - mapper.encode(PriceFloorRules.priceFloorRules) + mapper.encode(PriceFloorData.priceFloorData) } } 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 d82b329fa88..be81051b597 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,7 +1,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse @@ -33,10 +33,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -50,7 +49,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency } } @@ -66,10 +65,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the request.cur" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -78,7 +76,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Bid response with 2 bids: price < floorMin, price = floorMin" def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorValue, - floorsResponse.data.modelGroups[0].currency, bidRequest.cur[0]) + floorsResponse.modelGroups[0].currency, bidRequest.cur[0]) def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = EUR seatbid.first().bid << Bid.getDefaultBid(bidRequest.imp.first()) @@ -116,9 +114,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorMin, bidRequest.ext.prebid.floors.floorMinCur, floorProviderCur) - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): convertedMinFloorValue - 0.1] - data.modelGroups[0].currency = floorProviderCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): convertedMinFloorValue - 0.1] + modelGroups[0].currency = floorProviderCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -160,9 +158,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = EUR - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] - data.modelGroups[0].currency = floorsProviderCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] + modelGroups[0].currency = floorsProviderCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -179,7 +177,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { assert response.ext?.errors[ErrorType.GENERIC]*.code == [999] assert response.ext?.errors[ErrorType.GENERIC]*.message == ["Unable to convert from currency $bidRequest.ext.prebid.floors.floorMinCur to desired ad server" + - " currency ${floorsResponse.data.modelGroups[0].currency}" as String] + " currency ${floorsResponse.modelGroups[0].currency}" as String] and: "PBS should log a warning" assert response.ext?.warnings[PREBID]*.code == [999] @@ -256,10 +254,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the request.cur" def floorValue = PBSUtils.randomFloorValue def floorCur = USD - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = floorCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = floorCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -312,9 +309,9 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = BOGUS - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] - data.modelGroups[0].currency = floorsProviderCur + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): PBSUtils.randomFloorValue] + modelGroups[0].currency = floorsProviderCur } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) 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 617a8a78b61..bf6e6624de5 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 @@ -3,8 +3,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules -import org.prebid.server.functional.model.pricefloors.PriceFloorEnforcement +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors import org.prebid.server.functional.model.request.auction.BidRequest @@ -46,8 +45,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(ampRequest.account as String, floorsResponse) @@ -103,8 +102,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -143,6 +142,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { given: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] + ext.prebid.floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(enforcePbs: false)) } and: "Account with enabled fetch, fetch.url in the DB" @@ -151,9 +151,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(enforcePbs: false) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -231,6 +230,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Default basic BidRequest with generic bidder with preferdeals = true" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(preferdeals: true) + ext.prebid.floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(floorDeals: true)) } and: "Account with enabled fetch, fetch.url,enforceDealFloors in the DB" @@ -241,9 +241,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: true) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -280,6 +279,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Default basic BidRequest with generic bidder with preferdeals = true" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(preferdeals: true) + ext.prebid.floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(floorDeals: floorDeals, enforcePbs: enforcePbs)) } and: "Account with enabled fetch, fetch.url, enforceDealFloors in the DB" @@ -290,9 +290,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: floorDeals, enforcePbs: enforcePbs) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -347,9 +346,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: true) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -403,9 +401,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - enforcement = new PriceFloorEnforcement(floorDeals: true) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -452,8 +449,8 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 162cdf30d5e..3eed5329d8c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -2,8 +2,8 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.BidRequest @@ -17,7 +17,6 @@ import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY -import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP @@ -337,8 +336,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -534,8 +533,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response without modelGroups" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups = null } floorsProvider.setResponse(accountId, floorsResponse) @@ -576,8 +575,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response without rules" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = null } floorsProvider.setResponse(accountId, floorsResponse) @@ -621,8 +620,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response with 2 rules" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values.put(new Rule(mediaType: BANNER, country: MULTIPLE).rule, 0.7) + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values.put(new Rule(mediaType: BANNER, country: MULTIPLE).rule, 0.7) } floorsProvider.setResponse(accountId, floorsResponse) @@ -708,7 +707,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response with Content-Length" - def floorsResponse = PriceFloorRules.priceFloorRules + def floorsResponse = PriceFloorData.priceFloorData def responseSize = convertKilobyteSizeToByte(maxSize) + 100 floorsProvider.setResponse(accountId, floorsResponse, ["Content-Length": responseSize as String]) @@ -862,6 +861,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { given: "BidRequest with storedRequest" def bidRequest = bidRequestWithFloors.tap { ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.floors.floorMin = FLOOR_MIN } and: "Default stored request with floors" @@ -877,8 +877,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -889,19 +889,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider + ext?.prebid?.floors?.floorMin == bidRequest.ext.prebid.floors.floorMin + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse } } @@ -910,7 +910,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def ampRequest = AmpRequest.defaultAmpRequest and: "Default stored request with floors " - def ampStoredRequest = storedRequestWithFloors + def ampStoredRequest = storedRequestWithFloors.tap { + ext.prebid.floors.floorMin = FLOOR_MIN + } def storedRequest = StoredRequest.getDbStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -920,8 +922,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(ampRequest.account as String, floorsResponse) @@ -932,19 +934,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last() verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider + ext?.prebid?.floors?.floorMin == ampStoredRequest.ext.prebid.floors.floorMin + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse } } @@ -975,8 +977,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { where: description | floorsResponse - "valid" | PriceFloorRules.priceFloorRules - "invalid" | PriceFloorRules.priceFloorRules.tap { data.modelGroups = null } + "valid" | PriceFloorData.priceFloorData + "invalid" | PriceFloorData.priceFloorData.tap { modelGroups = null } } def "PBS should continue to hold onto previously fetched rules when fetch.enabled = false in account config"() { @@ -996,8 +998,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider #description response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -1019,18 +1021,18 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider + !ext?.prebid?.floors?.floorMin + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data + ext?.prebid?.floors?.data == floorsResponse } } @@ -1362,8 +1364,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider #description response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -1382,52 +1384,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { verifyAll(bidderRequest) { imp[0].bidFloor == floorValue - imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.data.modelGroups[0].values.keySet()[0] + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS - ext?.prebid?.floors?.floorMin == floorsResponse.floorMin - ext?.prebid?.floors?.floorProvider == floorsResponse.data.floorProvider + !ext?.prebid?.floors?.floorMin + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider ext?.prebid?.floors?.skipRate == floorsResponse.skipRate - ext?.prebid?.floors?.data == floorsResponse.data - } - } - - def "PBS should prefer floorMin from request over floorMin from fetched data"() { - given: "Default BidRequest" - def floorMin = PBSUtils.randomFloorValue - def floorMinCur = USD - def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { - ext.prebid.floors = new ExtPrebidFloors(floorMin: floorMin, floorMinCur: floorMinCur) - } - - and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id - def account = getAccountWithEnabledFetch(accountId) - accountDao.save(account) - - and: "Set Floors Provider #description response" - def floorValue = floorMin - 0.1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = floorMinCur - it.floorMin = floorValue + ext?.prebid?.floors?.data == floorsResponse } - floorsProvider.setResponse(accountId, floorsResponse) - - and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) - - when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) - - then: "Bidder request floorMin should correspond to floorMin from request" - def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert bidderRequest.ext?.prebid?.floors?.floorMin == floorMin } def "PBS should reject fetch when modelWeight from floors provider is invalid"() { @@ -1447,12 +1416,12 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups.first().modelWeight = invalidModelWeight - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().modelWeight = modelWeight + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups.first().modelWeight = invalidModelWeight + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().modelWeight = modelWeight } floorsProvider.setResponse(accountId, floorsResponse) @@ -1488,66 +1457,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] } - def "PBS should reject fetch when root skipRate from floors provider is invalid"() { - given: "Test start time" - def startTime = Instant.now() - - and: "Flush metrics" - flushMetrics(floorsPbsService) - - and: "Default BidRequest" - def bidRequest = BidRequest.defaultBidRequest - - and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id - def account = getAccountWithEnabledFetch(accountId) - accountDao.save(account) - - and: "Set Floors Provider response" - def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups[0].skipRate = 0 - data.skipRate = 0 - skipRate = invalidSkipRate - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().skipRate = 0 - } - floorsProvider.setResponse(accountId, floorsResponse) - - and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) - - when: "PBS processes auction request" - def response = floorsPbsService.sendAuctionRequest(bidRequest) - - and: "PBS processes collected metrics request" - def metrics = floorsPbsService.sendCollectedMetricsRequest() - - then: "Bidder request bidFloor should not be passed" - def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert !bidderRequest.imp[0].bidFloor - assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR - - and: "#FETCH_FAILURE_METRIC should be update" - assert metrics[FETCH_FAILURE_METRIC] == 1 - - and: "PBS log should contain error" - def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) - assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor root skipRate" + - " must be in range(0-100), but was $invalidSkipRate") - - and: "Floors validation failure cannot reject the entire auction" - assert !response.seatbid?.isEmpty() - - where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] - } - def "PBS should reject fetch when data skipRate from floors provider is invalid"() { given: "Test start time" def startTime = Instant.now() @@ -1565,14 +1474,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups[0].skipRate = 0 - data.skipRate = invalidSkipRate - skipRate = 0 - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().skipRate = 0 + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups[0].skipRate = 0 + skipRate = invalidSkipRate + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().skipRate = 0 } floorsProvider.setResponse(accountId, floorsResponse) @@ -1625,14 +1533,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups[0].skipRate = invalidSkipRate - data.skipRate = 0 + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups[0].skipRate = invalidSkipRate skipRate = 0 - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().skipRate = 0 + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().skipRate = 0 } floorsProvider.setResponse(accountId, floorsResponse) @@ -1686,12 +1593,12 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups[0].defaultFloor = invalidDefaultFloor - data.modelGroups.last().values = [(rule): floorValue] - data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups[0].defaultFloor = invalidDefaultFloor + modelGroups.last().values = [(rule): floorValue] + modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE } floorsProvider.setResponse(accountId, floorsResponse) @@ -1724,57 +1631,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert !response.seatbid?.isEmpty() } - def "PBS should reject fetch when floorMin from floors provider is invalid"() { - given: "Test start time" - def startTime = Instant.now() - - and: "Flush metrics" - flushMetrics(floorsPbsService) - - and: "Default BidRequest" - def bidRequest = BidRequest.defaultBidRequest - - and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id - def account = getAccountWithEnabledFetch(accountId) - accountDao.save(account) - - and: "Set Floors Provider response" - def invalidFloorMin = MIN_FLOOR_MIN - 1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = invalidFloorMin - } - floorsProvider.setResponse(accountId, floorsResponse) - - and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) - - when: "PBS processes auction request" - def response = floorsPbsService.sendAuctionRequest(bidRequest) - - and: "PBS processes collected metrics request" - def metrics = floorsPbsService.sendCollectedMetricsRequest() - - then: "Bidder request bidFloor should not be passed" - def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() - assert !bidderRequest.imp[0].bidFloor - assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR - - and: "#FETCH_FAILURE_METRIC should be update" - assert metrics[FETCH_FAILURE_METRIC] == 1 - - and: "PBS log should contain error" - def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) - assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " + - "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor floorMin" + - " must be positive float, but was $invalidFloorMin") - - and: "Floors validation failure cannot reject the entire auction" - assert !response.seatbid?.isEmpty() - } - def "PBS should give preference to currency from modelGroups when signalling"() { given: "Default BidRequest with floors" def bidRequest = bidRequestWithFloors @@ -1785,11 +1641,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - floorMin = floorValue - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = modelGroupCurrency - data.currency = dataCurrency + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = modelGroupCurrency + currency = dataCurrency } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) 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 6521cda5a57..1c6d149b824 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 @@ -1,9 +1,10 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules + import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.auction.App @@ -56,9 +57,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { def floorValue = 0.8 def invalidRule = new Rule(mediaType: BANNER, country: Country.MULTIPLE, siteDomain: PBSUtils.randomString).rule - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY]) - data.modelGroups[0].values = [(rule) : floorValue, + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY]) + modelGroups[0].values = [(rule) : floorValue, (invalidRule): floorValue + 0.1] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -86,9 +87,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY], delimiter: delimiter) - data.modelGroups[0].values = [(new Rule(delimiter: delimiter, mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE).rule) : PBSUtils.randomFloorValue, + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE, COUNTRY], delimiter: delimiter) + modelGroups[0].values = [(new Rule(delimiter: delimiter, mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE).rule) : PBSUtils.randomFloorValue, (new Rule(delimiter: delimiter, mediaType: BANNER, country: Country.MULTIPLE).rule): floorValue]} floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -133,9 +134,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[0].values = [(new Rule(domain: domain).rule.toUpperCase()) : floorValue, (new Rule(domain: PBSUtils.randomString).rule.toUpperCase()): floorValue + 0.1] } @@ -166,12 +167,12 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups[0].schema = new PriceFloorSchema(fields: [BOGUS]) - data.modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue + 0.1] - data.modelGroups[1].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[1].values = [(new Rule(domain: domain).rule) : floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups[0].schema = new PriceFloorSchema(fields: [BOGUS]) + modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue + 0.1] + modelGroups[1].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[1].values = [(new Rule(domain: domain).rule) : floorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -215,9 +216,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.getRoundedFractionalNumber(PBSUtils.getFractionalRandomNumber(FLOOR_MIN, 2), 6) as BigDecimal - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -238,9 +239,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) + modelGroups[0].values = [(new Rule(mediaType: MediaType.MULTIPLE).rule): bothFloorValue, (new Rule(mediaType: BANNER).rule) : bannerFloorValue, (new Rule(mediaType: VIDEO).rule) : videoFloorValue] @@ -282,9 +283,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def requestFloorValue = 0.8 def floorsProviderFloorValue = requestFloorValue + 0.1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) + modelGroups[0].values = [(new Rule(size: "*").rule) : floorsProviderFloorValue, (new Rule(size: "${lowerWidth}x${lowerHigh}").rule) : floorsProviderFloorValue + 0.1, (new Rule(size: "${higherWidth}x${higherHigh}").rule): floorsProviderFloorValue + 0.2] @@ -315,9 +316,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def requestFloorValue = 0.8 def floorsProviderFloorValue = requestFloorValue + 0.1 - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [SIZE]) + modelGroups[0].values = [(new Rule(size: "*").rule) : floorsProviderFloorValue + 0.1, (new Rule(size: "${width}x${height}").rule): floorsProviderFloorValue] } @@ -358,9 +359,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DOMAIN]) + modelGroups[0].values = [(new Rule(domain: domain).rule) : floorValue, (new Rule(domain: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -407,9 +408,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) + modelGroups[0].values = [(new Rule(siteDomain: domain).rule) : floorValue, (new Rule(siteDomain: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -448,9 +449,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [PUB_DOMAIN]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [PUB_DOMAIN]) + modelGroups[0].values = [(new Rule(pubDomain: domain).rule) : floorValue, (new Rule(pubDomain: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -490,9 +491,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [BUNDLE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [BUNDLE]) + modelGroups[0].values = [(new Rule(bundle: bundle).rule) : floorValue, (new Rule(bundle: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -522,9 +523,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [CHANNEL]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [CHANNEL]) + modelGroups[0].values = [(new Rule(channel: channel).rule) : floorValue, (new Rule(channel: APP).rule): floorValue + 0.1] } @@ -552,9 +553,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [GPT_SLOT]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [GPT_SLOT]) + modelGroups[0].values = [(new Rule(gptSlot: gptSlot).rule) : floorValue, (new Rule(gptSlot: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -590,9 +591,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [PB_AD_SLOT]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [PB_AD_SLOT]) + modelGroups[0].values = [(new Rule(pbAdSlot: pbAdSlot).rule) : floorValue, (new Rule(pbAdSlot: PBSUtils.randomString).rule): floorValue + 0.1] } @@ -622,9 +623,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [COUNTRY]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [COUNTRY]) + modelGroups[0].values = [(new Rule(country: country).rule) : floorValue, (new Rule(country: Country.MULTIPLE).rule): floorValue + 0.1] } @@ -652,9 +653,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) + modelGroups[0].values = [(new Rule(deviceType: PHONE).rule): phoneFloorValue, (new Rule(deviceType: TABLET).rule): tabletFloorValue, (new Rule(deviceType: DESKTOP).rule): desktopFloorValue, @@ -698,9 +699,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with wildcard deviceType rule" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) - data.modelGroups[0].values = + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [DEVICE_TYPE]) + modelGroups[0].values = [(new Rule(deviceType: PHONE).rule): floorValue + 0.1, (new Rule(deviceType: TABLET).rule): floorValue + 0.2, (new Rule(deviceType: DESKTOP).rule): floorValue + 0.3, @@ -729,10 +730,10 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) - data.modelGroups[0].values = [(new Rule(mediaType: VIDEO).rule): floorValue + 0.1] - data.modelGroups[0].defaultFloor = floorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) + modelGroups[0].values = [(new Rule(mediaType: VIDEO).rule): floorValue + 0.1] + modelGroups[0].defaultFloor = floorValue } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) 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 9024b5aa1ac..36dd199a9e3 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 @@ -1,9 +1,9 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.mock.services.floorsprovider.PriceFloorRules import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.amp.AmpRequest @@ -92,8 +92,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorsProviderFloorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorsProviderFloorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -118,8 +118,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set invalid Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = null } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -146,8 +146,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set invalid Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = null + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = null } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -178,10 +178,10 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with skipRate" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD - data.modelGroups[0].skipRate = skipRate + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD + modelGroups[0].skipRate = skipRate } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -194,7 +194,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { then: "Bidder request bidFloor should correspond to floors provider" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - assert bidderRequest.imp[0].bidFloorCur == floorsResponse.data.modelGroups[0].currency + assert bidderRequest.imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency assert !bidderRequest.ext?.prebid?.floors?.skipped where: @@ -214,12 +214,11 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response with skipRate" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] - data.modelGroups[0].currency = USD - data.modelGroups[0].skipRate = modelGroupSkipRate - data.skipRate = dataSkipRate - skipRate = rootSkipRate + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + modelGroups[0].currency = USD + modelGroups[0].skipRate = modelGroupSkipRate + skipRate = dataSkipRate } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -240,10 +239,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert !bidderRequest.imp[0].ext?.prebid?.floors where: - modelGroupSkipRate | dataSkipRate | rootSkipRate - 100 | 0 | 0 - null | 100 | 0 - null | null | 100 + modelGroupSkipRate | dataSkipRate + 100 | 0 + null | 100 } def "PBS should not emit error when request has more rules than fetch.max-rules"() { @@ -344,8 +342,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorsProviderFloorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorsProviderFloorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -391,8 +389,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { accountDao.save(account) and: "Set Floors Provider response" - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorsProviderFloorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorsProviderFloorValue] } floorsProvider.setResponse(accountId, floorsResponse) @@ -428,8 +426,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].values = [(rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) @@ -459,11 +457,11 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups << ModelGroup.modelGroup - data.modelGroups.first().values = [(rule): floorValue + 0.1] - data.modelGroups.last().schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) - data.modelGroups.last().values = [(new Rule(siteDomain: domain).rule): floorValue] + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups << ModelGroup.modelGroup + modelGroups.first().values = [(rule): floorValue + 0.1] + modelGroups.last().schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) + modelGroups.last().values = [(new Rule(siteDomain: domain).rule): floorValue] } floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) @@ -488,9 +486,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def bannerFloorValue = PBSUtils.randomFloorValue def videoFloorValue = PBSUtils.randomFloorValue - def floorsResponse = PriceFloorRules.priceFloorRules.tap { - data.modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) - data.modelGroups[0].values = [(new Rule(mediaType: BANNER).rule): bannerFloorValue, + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].schema = new PriceFloorSchema(fields: [MEDIA_TYPE]) + modelGroups[0].values = [(new Rule(mediaType: BANNER).rule): bannerFloorValue, (new Rule(mediaType: VIDEO).rule) : videoFloorValue] } floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java index 30606df9170..fa639324124 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java @@ -12,7 +12,6 @@ import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorLocation; @@ -30,7 +29,6 @@ import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.function.UnaryOperator; @@ -38,8 +36,6 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -53,8 +49,6 @@ public class BasicPriceFloorProcessorTest extends VertxTest { private PriceFloorFetcher priceFloorFetcher; @Mock private PriceFloorResolver floorResolver; - @Mock - private CurrencyConversionService conversionService; private BasicPriceFloorProcessor priceFloorProcessor; @@ -63,7 +57,6 @@ public void setUp() { priceFloorProcessor = new BasicPriceFloorProcessor( priceFloorFetcher, floorResolver, - conversionService, jacksonMapper); } @@ -112,16 +105,18 @@ public void shouldSetRulesEnabledFieldToFalseIfPriceFloorsDisabledForRequest() { } @Test - public void shouldUseFloorsFromProviderIfPresent() { + public void shouldUseFloorsDataFromProviderIfPresent() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(identity()), givenBidRequest( identity(), - null)); + givenFloors(floors -> floors.floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ONE)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -130,7 +125,9 @@ public void shouldUseFloorsFromProviderIfPresent() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .enabled(true) + .floorProvider("provider.com") .floorMin(BigDecimal.ONE) + .data(providerFloorsData) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); } @@ -144,8 +141,10 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { identity(), null)); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ONE)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -154,22 +153,25 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .enabled(true) - .floorMin(BigDecimal.ONE) + .floorProvider("provider.com") + .data(providerFloorsData) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); } @Test - public void shouldNUseFloorsFromProviderIfUseDynamicDataIsTrue() { + public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() { // given final AuctionContext auctionContext = givenAuctionContext( givenAccount(floorsConfig -> floorsConfig.useDynamicData(true)), givenBidRequest( identity(), - null)); + givenFloors(floors -> floors.floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ONE)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -178,6 +180,8 @@ public void shouldNUseFloorsFromProviderIfUseDynamicDataIsTrue() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .enabled(true) + .floorProvider("provider.com") + .data(providerFloorsData) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); @@ -192,8 +196,10 @@ public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() { identity(), null)); - final PriceFloorRules providerFloors = givenFloors(identity()); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -219,8 +225,10 @@ public void shouldMergeProviderWithRequestFloors() { .enforcement(PriceFloorEnforcement.builder().enforcePbs(false).enforceRate(100).build()) .floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(BigDecimal.ZERO)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -229,7 +237,12 @@ public void shouldMergeProviderWithRequestFloors() { assertThat(extractFloors(result)) .isEqualTo(givenFloors(floors -> floors .enabled(true) - .enforcement(PriceFloorEnforcement.builder().enforceRate(100).build()) + .floorProvider("provider.com") + .enforcement(PriceFloorEnforcement.builder() + .enforcePbs(false) + .enforceRate(100 + ).build()) + .data(providerFloorsData) .floorMin(BigDecimal.ONE) .fetchStatus(FetchStatus.success) .location(PriceFloorLocation.fetch))); @@ -244,18 +257,22 @@ public void shouldReturnProviderFloorsWhenNotEnabledByRequestAndEnforceRateAndFl identity(), givenFloors(floors -> floors.data(givenFloorData(identity())).enabled(null)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors.floorMin(null).floorMinCur(null)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); // then - final PriceFloorRules expectedResult = providerFloors.toBuilder() - .enabled(true) - .fetchStatus(FetchStatus.success) - .location(PriceFloorLocation.fetch) - .build(); + final PriceFloorRules expectedResult = + givenFloors(floors -> floors + .enabled(true) + .floorProvider("provider.com") + .data(providerFloorsData) + .fetchStatus(FetchStatus.success) + .location(PriceFloorLocation.fetch)); assertThat(extractFloors(result)).isEqualTo(expectedResult); } @@ -272,8 +289,10 @@ public void shouldReturnFloorsWithFloorMinAndCurrencyFromRequestWhenPresent() { .floorMin(BigDecimal.ONE) .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("USD")))))); - final PriceFloorRules providerFloors = givenFloors(identity()); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -284,118 +303,6 @@ public void shouldReturnFloorsWithFloorMinAndCurrencyFromRequestWhenPresent() { .containsExactly(BigDecimal.ONE, "USD"); } - @Test - public void shouldReturnFloorsWithFloorCurrencyFromRequestAndFloorMinFromProviderWhenRequestFloorMinAbsent() { - // given - final AuctionContext auctionContext = givenAuctionContext( - givenAccount(identity()), - givenBidRequest( - identity(), - givenFloors(floors -> floors - .enabled(true) - .floorMinCur("USD")))); - - final PriceFloorRules providerFloors = givenFloors(floors -> - floors.floorMin(BigDecimal.ONE) - .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("USD")))); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); - - // when - final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); - - // then - assertThat(extractFloors(result)) - .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) - .containsExactly(BigDecimal.ONE, "USD"); - } - - @Test - public void shouldReturnFloorsWithConvertedToRequestCurrencyProviderFloorMin() { - // given - final AuctionContext auctionContext = givenAuctionContext( - givenAccount(identity()), - givenBidRequest( - identity(), - givenFloors(floors -> floors - .enabled(true) - .floorMinCur("USD")))); - - final PriceFloorRules providerFloors = givenFloors(floors -> - floors.floorMin(BigDecimal.ONE) - .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("UAH")))); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); - given(conversionService.convertCurrency( - eq(BigDecimal.ONE), - eq(Collections.emptyMap()), - eq("UAH"), - eq("USD"), - eq(false))) - .willReturn(BigDecimal.valueOf(2)); - // when - final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); - - // then - assertThat(extractFloors(result)) - .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) - .containsExactly(BigDecimal.valueOf(2), "USD"); - } - - @Test - public void shouldReturnFloorsWithConvertedToProviderCurrencyRequestFloorMinFromDefaultUsd() { - // given - final AuctionContext auctionContext = givenAuctionContext( - givenAccount(identity()), - givenBidRequest( - identity(), - givenFloors(floors -> floors - .enabled(true) - .floorMin(BigDecimal.ONE)))); - - final PriceFloorRules providerFloors = givenFloors(floors -> - floors.floorMin(BigDecimal.valueOf(2)) - .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("UAH")))); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); - given(conversionService.convertCurrency( - eq(BigDecimal.ONE), - eq(Collections.emptyMap()), - isNull(), - eq("UAH"), - eq(false))) - .willReturn(BigDecimal.valueOf(2)); - // when - final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); - - // then - assertThat(extractFloors(result)) - .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) - .containsExactly(BigDecimal.valueOf(2), null); - } - - @Test - public void shouldReturnFloorsWithRequestFloorMinCurrencyAndProviderFloorMin() { - // given - final AuctionContext auctionContext = givenAuctionContext( - givenAccount(identity()), - givenBidRequest( - identity(), - givenFloors(floors -> floors - .enabled(true) - .floorMinCur("USD")))); - - final PriceFloorRules providerFloors = givenFloors(floors -> - floors.floorMin(BigDecimal.valueOf(-2)) - .data(givenFloorData(floorsDataConfig -> floorsDataConfig.currency("UAH")))); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); - - // when - final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); - - // then - assertThat(extractFloors(result)) - .extracting(PriceFloorRules::getFloorMin, PriceFloorRules::getFloorMinCur) - .containsExactly(BigDecimal.valueOf(-2), "USD"); - } - @Test public void shouldUseFloorsFromRequestIfProviderFloorsMissing() { // given @@ -589,10 +496,10 @@ public void shouldCopyFloorProviderValueFromDataLevel() { givenFloors(floors -> floors .floorMin(BigDecimal.ONE)))); - final PriceFloorRules providerFloors = givenFloors(floors -> floors - .data(PriceFloorData.builder().floorProvider("someProvider").build()) - .floorMin(BigDecimal.ZERO)); - given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloors, FetchStatus.success)); + final PriceFloorData providerFloorsData = + givenFloorData(floors -> floors.floorProvider("provider.com")); + given(priceFloorFetcher.fetch(any())) + .willReturn(FetchResult.of(providerFloorsData, FetchStatus.success)); // when final AuctionContext result = priceFloorProcessor.enrichWithPriceFloors(auctionContext); @@ -601,10 +508,7 @@ public void shouldCopyFloorProviderValueFromDataLevel() { assertThat(extractFloors(result)) .extracting(PriceFloorRules::getData) .extracting(PriceFloorData::getFloorProvider) - .isEqualTo("someProvider"); - assertThat(extractFloors(result)) - .extracting(PriceFloorRules::getFloorProvider) - .isEqualTo("someProvider"); + .isEqualTo("provider.com"); } @Test diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java index d18eb51f9da..182ad127e54 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java @@ -15,11 +15,11 @@ import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.model.PriceFloorField; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.floors.model.PriceFloorSchema; -import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.proto.FetchResult; import org.prebid.server.floors.proto.FetchStatus; import org.prebid.server.metric.Metrics; @@ -90,7 +90,7 @@ public void fetchShouldReturnPriceFloorFetchedFromProviderAndCache() { final Account givenAccount = givenAccount(identity()); given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when final FetchResult fetchResult = priceFloorFetcher.fetch(givenAccount); @@ -104,7 +104,7 @@ public void fetchShouldReturnPriceFloorFetchedFromProviderAndCache() { final FetchResult priceFloorRulesCached = priceFloorFetcher.fetch(givenAccount); assertThat(priceFloorRulesCached.getFetchStatus()).isEqualTo(FetchStatus.success); - assertThat(priceFloorRulesCached.getRules()).isEqualTo(givenPriceFloorRules()); + assertThat(priceFloorRulesCached.getRulesData()).isEqualTo(givenPriceFloorData()); } @@ -118,7 +118,7 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocation( final FetchResult fetchResult = priceFloorFetcher.fetch(givenAccount(identity())); // then - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); } @@ -133,12 +133,12 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocationA final FetchResult firstInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); // then - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); } @@ -152,12 +152,12 @@ public void fetchShouldReturnEmptyRulesAndInProgressStatusForTheFirstInvocationA final FetchResult firstInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); // then - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.timeout); } @@ -168,7 +168,7 @@ public void fetchShouldCacheResponseForTimeFromResponseCacheControlHeader() { .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CACHE_CONTROL, "max-age=700"), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -185,7 +185,7 @@ public void fetchShouldTakePrecedenceForTestingPropertyToCacheResponse() { .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CACHE_CONTROL, "max-age=700"), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -202,7 +202,7 @@ public void fetchShouldTakePrecedenceForTestingPropertyToCreatePeriodicTimer() { given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -219,7 +219,7 @@ public void fetchShouldTakePrecedenceForTestingPropertyToChooseRequestTimeout() given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -236,7 +236,7 @@ public void fetchShouldTakePrecedenceForMinTimeoutTestingPropertyToChooseRequest given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -251,7 +251,7 @@ public void fetchShouldSetDefaultCacheTimeWhenCacheControlHeaderCantBeParsed() { given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CACHE_CONTROL, "invalid"), - jacksonMapper.encodeToString(givenPriceFloorRules())))); + jacksonMapper.encodeToString(givenPriceFloorData())))); // when priceFloorFetcher.fetch(givenAccount(identity())); @@ -267,7 +267,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsMalformedAndReturnErro // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoInteractions(vertx); } @@ -279,7 +279,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsBlankAndReturnErrorSta // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoInteractions(vertx); } @@ -291,7 +291,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchUrlIsNotProvidedAndReturnEr // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoInteractions(vertx); } @@ -303,7 +303,7 @@ public void fetchShouldNotPrepareAnyRequestsWhenFetchEnabledIsFalseAndReturnNone // then verifyNoInteractions(httpClient); - assertThat(fetchResult.getRules()).isNull(); + assertThat(fetchResult.getRulesData()).isNull(); assertThat(fetchResult.getFetchStatus()).isEqualTo(FetchStatus.none); verifyNoInteractions(vertx); } @@ -320,12 +320,12 @@ public void fetchShouldReturnEmptyRulesAndErrorStatusForSecondCallAndCreatePerio // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -342,12 +342,12 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusAndCreatePeriodicTimerWhen // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -364,12 +364,12 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusForSecondCallAndCreatePeri // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -386,12 +386,12 @@ public void fetchShouldReturnEmptyRulesWithErrorStatusForSecondCallAndCreatePeri // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -410,16 +410,16 @@ public void fetchShouldNotCallPriceFloorProviderWhileFetchIsAlreadyInProgress() verify(httpClient).get(anyString(), anyLong(), anyLong()); verifyNoMoreInteractions(httpClient); - assertThat(secondFetch.getRules()).isNull(); + assertThat(secondFetch.getRulesData()).isNull(); assertThat(secondFetch.getFetchStatus()).isEqualTo(FetchStatus.inprogress); fetchPromise.tryComplete( HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap() .add(HttpHeaders.CACHE_CONTROL, "max-age==3"), - jacksonMapper.encodeToString(givenPriceFloorRules()))); + jacksonMapper.encodeToString(givenPriceFloorData()))); - final PriceFloorRules thirdFetch = priceFloorFetcher.fetch(givenAccount(identity())).getRules(); - assertThat(thirdFetch).isEqualTo(givenPriceFloorRules()); + final PriceFloorData thirdFetch = priceFloorFetcher.fetch(givenAccount(identity())).getRulesData(); + assertThat(thirdFetch).isEqualTo(givenPriceFloorData()); } @Test @@ -428,13 +428,11 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules given(httpClient.get(anyString(), anyLong(), anyLong())) .willReturn(Future.succeededFuture(HttpClientResponse.of(200, MultiMap.caseInsensitiveMultiMap(), - jacksonMapper.encodeToString(givenPriceFloorRules().toBuilder() - .data(PriceFloorData.builder() + jacksonMapper.encodeToString(PriceFloorData.builder() .modelGroups(singletonList(PriceFloorModelGroup.builder() .value("video", BigDecimal.ONE).value("banner", BigDecimal.TEN) .build())) - .build()) - .build())))); + .build())))); // when final FetchResult firstInvocationResult = @@ -442,12 +440,12 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules // then verify(httpClient).get(anyString(), anyLong(), anyLong()); - assertThat(firstInvocationResult.getRules()).isNull(); + assertThat(firstInvocationResult.getRulesData()).isNull(); assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress); verify(vertx).setTimer(eq(1700000L), any()); verify(vertx).setTimer(eq(1500000L), any()); final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity())); - assertThat(secondInvocationResult.getRules()).isNull(); + assertThat(secondInvocationResult.getRulesData()).isNull(); assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error); verifyNoMoreInteractions(vertx); } @@ -478,17 +476,14 @@ private static AccountPriceFloorsFetchConfig givenFetchConfig( .build(); } - private PriceFloorRules givenPriceFloorRules() { - return PriceFloorRules.builder() - .data(PriceFloorData.builder() - .currency("USD") - .modelGroups(singletonList(PriceFloorModelGroup.builder() - .modelVersion("model version 1.0") - .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) - .value("banner", BigDecimal.TEN) - .currency("EUR").build())) - .build()) - .skipRate(60) + private PriceFloorData givenPriceFloorData() { + return PriceFloorData.builder() + .currency("USD") + .modelGroups(singletonList(PriceFloorModelGroup.builder() + .modelVersion("model version 1.0") + .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType))) + .value("banner", BigDecimal.TEN) + .currency("EUR").build())) .build(); } } diff --git a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java index 6c6082c3db4..90689b73ce1 100644 --- a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java +++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java @@ -25,7 +25,7 @@ public void validateShouldThrowExceptionOnInvalidRootSkipRateWhenPresent() { final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.skipRate(-1)); assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor root skipRate must be in range(0-100), but was -1"); } @@ -37,7 +37,7 @@ public void validateShouldThrowExceptionWhenFloorMinPresentAndLessThanZero() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor floorMin must be positive float, but was -1"); } @@ -48,7 +48,7 @@ public void validateShouldThrowExceptionWhenDataIsAbsent() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor rules data must be present"); } @@ -59,7 +59,7 @@ public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor data skipRate must be in range(0-100), but was -1"); } @@ -71,7 +71,7 @@ public void validateShouldThrowExceptionOnAbsentDataModelGroups() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor rules should contain at least one model group"); } @@ -83,7 +83,7 @@ public void validateShouldThrowExceptionOnEmptyDataModelGroups() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor rules should contain at least one model group"); } @@ -95,7 +95,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupModelWeightWhenPr // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor modelGroup modelWeight must be in range(1-100), but was -1"); } @@ -107,7 +107,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupSkipRateWhenPrese // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor modelGroup skipRate must be in range(0-100), but was -1"); } @@ -119,7 +119,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupDefaultFloorWhenP // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor modelGroup default must be positive float, but was -1"); } @@ -131,7 +131,7 @@ public void validateShouldThrowExceptionOnEmptyModelGroupValues() { // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, 100)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100)) .withMessage("Price floor rules values can't be null or empty, but were {}"); } @@ -149,7 +149,7 @@ public void validateShouldThrowExceptionWhenModelGroupValuesSizeGreaterThanMaxRu // when and then assertThatExceptionOfType(PreBidException.class) - .isThrownBy(() -> PriceFloorRulesValidator.validate(priceFloorRules, maxRules)) + .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, maxRules)) .withMessage( "Price floor rules number %s exceeded its maximum number %s", modelGroupValues.size(), diff --git a/src/test/resources/org/prebid/server/functional/floor-rules.json b/src/test/resources/org/prebid/server/functional/floor-rules.json index 8bd346f25bf..194c83ebc91 100644 --- a/src/test/resources/org/prebid/server/functional/floor-rules.json +++ b/src/test/resources/org/prebid/server/functional/floor-rules.json @@ -1,157 +1,155 @@ { - "data": { - "floorProvider": "rubicon", - "modelGroups": [ - { - "modelWeight": 10, - "modelVersion": "mlcp-v1@2022-03-02-21", - "schema": { - "fields": [ - "domain", - "mediaType", - "gptSlot" - ], - "delimiter": "|" - }, - "values": { - "example.com|banner|/111/k/categorytop/footer_left/300x250": 0.07, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_15": 0.02, - "s.example.com|banner|/111/ks/categorytop/300x250": 0.02, - "bbs.example.com|banner|/111/k/itemview/bbs/footer_left/300x250": 0.04, - "example5.com|banner|/111/t/shop/300x600": 0.02, - "example5.com|banner|/111/t/list/search_footer_left_300x250": 0.03, - "example2.com|banner|/111/kinarinopc/article/300x250": 0.18, - "s.example.com|banner|/111/ks/itemview/h/footer/300x250": 0.03, - "example6.com|banner|/111/cg/top_3rd_300x250": 0.06, - "example5.com|banner|/111/t/shop/1st300x250": 0.02, - "example.com|banner|/111/k/ranking/footer_right/300x250": 0.06, - "review.example.com|banner|/111/k/itemview/review/footer_left/300x250": 0.05, - "bbs.example.com|banner|/111/k/itemview/footer_right/300x250": 0.04, - "example7.com|banner|/111/e/contents/footer_left_300x250": 0.02, - "s.example.com|banner|/111/ks/news/300x250": 0.03, - "s.example.com|banner|/111/ks/categorytop/footer/300x250": 0.02, - "example6.com|banner|/111/cgs/ros/300x250": 0.01, - "example7.com|banner|/111/e/contents/footer_728x90": 0.06, - "example.com|banner|/111/k/categorytop/footer_right/300x250": 0.06, - "s.example.com|banner|/111/ks/top_320x50": 0.02, - "example5.com|banner|/111/t/map/middle_468x60": 0.04, - "example4.com|*|*": 0.07, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_6": 0.06, - "news.example.com|banner|/111/k/news/footer_right/300x250": 0.08, - "example4.com|banner|/111/kmag/1st_300x250": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_4": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_1": 0.07, - "example.com|banner|/111/k/btf/tv/footer_right_300x250": 0.01, - "s.example5.com|banner|/111/ts/list/300x250": 0.02, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_5": 0.06, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_10": 0.03, - "example.com|banner|/111/k/global_search/footer_right/300x250": 0.06, - "s.akiba-souken.com|banner|/111/as/1st_300x250": 0.03, - "bbs.example.com|banner|/111/k/itemview/bbs/160x600": 0.06, - "example.com|banner|/111/k/tv_728x90": 0.06, - "example3.com|*|*": 0.02, - "s.example.com|banner|/111/ks/ranking/middle_20/300x250": 0.05, - "s.example.com|banner|/111/ks/itemview/review/300x250_9": 0.03, - "example5.com|banner|/111/t/map/middle_left_300x250": 0.04, - "s.example.com|banner|/111/ks/itemview/review/300x250_12": 0.04, - "example.com|banner|/111/k/ranking/728x90": 0.06, - "example6.com|banner|/111/cg/ros/footer_right_300x250": 0.07, - "example6.com|banner|/111/cg/top_300x250": 0.09, - "example7.com|banner|/111/es/overlay/320x50": 0.07, - "s.example.com|banner|/111/ks/itemview/bbs/300x250": 0.08, - "example5.com|banner|/111/t/list/search_footer_right_300x250": 0.04, - "example.com|banner|/111/k/pricemenu/728x90": 0.06, - "s.example.com|banner|/111/ks/itemview/320x50_lazytest": 0.03, - "search.example.com|banner|/111/ks/itemlist/320x50": 0.03, - "s.example.com|banner|/111/ks/categorytop/middle/320x50": 0.06, - "example.com|*|*": 0.04, - "example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/footer_right/300x250": 0.04, - "s.example.com|banner|/111/ks/itemview/review/300x250_3": 0.05, - "example4.com|banner|/111/kmag/footer_left_300x250": 0.08, - "s.example.com|banner|/111/ks/itemview/bbs/300x250_2": 0.08, - "example.com|banner|/111/k/itemlist/728x90": 0.07, - "example.com|banner|/111/k/ranking/middle/left/300x250": 0.09, - "search.example.com|banner|/111/k/itemlist/160x600": 0.12, - "example2.com|banner|/111/kinarinopc/top_300x250": 0.01, - "s.example.com|banner|/111/ks/itemlist/footer/300x250": 0.02, - "example2.com|banner|/111/kinarino/login": 0.09, - "example5.com|banner|/111/t/special/4th_300x250": 0.02, - "s.example.com|banner|/111/ks/news/320x50": 0.05, - "s.example.com|banner|/111/ks/itemview/review/300x250_6": 0.03, - "example.com|banner|/111/k/ranking/footer_left/300x250": 0.03, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_2": 0.06, - "example4.com|banner|/111/kmag/3rd_300x250": 0.08, - "s.example.com|banner|/111/ks/itemview/300x250": 0.04, - "example.com|banner|/111/k/itemview/footer_left/300x250": 0.05, - "review.example.com|banner|/111/k/itemview/review/160x600": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_5": 0.06, - "example5.com|banner|/111/t/shop/shop_footer_left_300x250": 0.02, - "s.example5.com|banner|/111/ts/shop/middle/300x250": 0.04, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_6": 0.01, - "s.example.com|banner|/111/ks/ranking/middle_30/300x250": 0.03, - "example.com|banner|/111/k/top_2nd_300x250": 0.01, - "search.example.com|banner|/111/ks/itemlist/middle/320x50": 0.07, - "search.example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, - "s.example.com|banner|/111/ks/categorytop/320x50": 0.02, - "s.example.com|banner|/111/ks/pricemenu/320x50": 0.02, - "example.com|banner|/111/k/specsearch/footer/728x90": 0.01, - "example4.com|banner|/111/4ts/ros/video_320x180": 0.03, - "example5.com|banner|/111/t/matome/article/300x250": 0.04, - "s.example.com|banner|/111/ks/ranking/middle_10/300x250": 0.07, - "s.example.com|banner|/111/ks/ranking/320x50": 0.03, - "example.com|banner|/111/k/categorytop/300x250": 0.08, - "s.example.com|banner|/111/ks/itemview/h/320x50": 0.03, - "example2.com|banner|/111/kinarino/article": 0.13, - "anime.example7.com|banner|/111/ahs/overlay/320x50": 0.09, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_3": 0.06, - "example.com|banner|/111/k/pricemenu/footer_right/300x250": 0.06, - "s.example.com|banner|/111/ks/itemview/bbs/footer/300x250": 0.03, - "s.example.com|banner|/111/ks/global_search/300x250": 0.06, - "example2.com|*|*": 0.03, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_5": 0.08, - "review.example.com|*|*": 0.04, - "s.example.com|banner|/111/ks/itemview/review/footer/300x250": 0.03, - "example6.com|banner|/111/cg/ros/footer_left_300x250": 0.04, - "search.example.com|banner|/111/k/itemlist/footer_left/300x250": 0.04, - "example.com|banner|/111/k/itemview/h/160x600": 0.06, - "review.example.com|banner|/111/k/itemview/review/728x90": 0.05, - "s.example.com|banner|/111/ks/tv/overlay_320x50": 0.04, - "news.example.com|*|*": 0.07, - "example.com|banner|/111/k/ranking/middle/right/300x250": 0.03, - "s.example.com|banner|/111/ks/itemlist/300x250": 0.03, - "example6.com|*|*": 0.02, - "example4.com|banner|/111/ksmag/footer_300x250": 0.06, - "example7.com|banner|/111/e/overlay/728x90": 0.03, - "example4.com|banner|/111/kmag/2nd_300x250": 0.09, - "s.example.com|banner|/111/ks/itemview/review/300x250": 0.04, - "example.com|banner|/111/k/categorytop/728x90": 0.06, - "example5.com|banner|/111/t/shop/shop_footer_right_300x250": 0.02, - "bbs.example.com|banner|/111/k/itemview/footer_left/300x250": 0.04, - "*|*|*": 0.01, - "bbs.example.com|*|*": 0.02, - "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_20": 0.06, - "example3.com|banner|/111/icotto_sp/article/footer_1st300x250": 0.04, - "s.example.com|banner|/111/ks/itemlist/320x50": 0.04, - "s.example.com|banner|/111/ks/itemview/footer/300x250": 0.03, - "s.example.com|banner|/111/ks/itemview/h/300x250": 0.03, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_3": 0.06, - "example3.com|banner|/111/icotto_pc/2nd_300x250": 0.08, - "example.com|banner|/111/k/pricemenu/300x250": 0.06, - "s.example.com|banner|/111/ks/tv/middle_300x250": 0.03, - "example7.com|banner|/111/es/contents/footer_buzz_300x250": 0.02, - "example.com|banner|/111/k/itemview/spec/160x600": 0.07, - "example5.com|banner|/111/t/map/middle_right_300x250": 0.04, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_2": 0.06, - "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_4": 0.06, - "search.example.com|*|*": 0.02 - }, - "default": 0.01 - } - ], - "modelTimestamp": 1646254800, - "currency": "USD", - "skipRate": 0, - "floorsSchemaVersion": 2 - } + "floorProvider": "rubicon", + "modelGroups": [ + { + "modelWeight": 10, + "modelVersion": "mlcp-v1@2022-03-02-21", + "schema": { + "fields": [ + "domain", + "mediaType", + "gptSlot" + ], + "delimiter": "|" + }, + "values": { + "example.com|banner|/111/k/categorytop/footer_left/300x250": 0.07, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_15": 0.02, + "s.example.com|banner|/111/ks/categorytop/300x250": 0.02, + "bbs.example.com|banner|/111/k/itemview/bbs/footer_left/300x250": 0.04, + "example5.com|banner|/111/t/shop/300x600": 0.02, + "example5.com|banner|/111/t/list/search_footer_left_300x250": 0.03, + "example2.com|banner|/111/kinarinopc/article/300x250": 0.18, + "s.example.com|banner|/111/ks/itemview/h/footer/300x250": 0.03, + "example6.com|banner|/111/cg/top_3rd_300x250": 0.06, + "example5.com|banner|/111/t/shop/1st300x250": 0.02, + "example.com|banner|/111/k/ranking/footer_right/300x250": 0.06, + "review.example.com|banner|/111/k/itemview/review/footer_left/300x250": 0.05, + "bbs.example.com|banner|/111/k/itemview/footer_right/300x250": 0.04, + "example7.com|banner|/111/e/contents/footer_left_300x250": 0.02, + "s.example.com|banner|/111/ks/news/300x250": 0.03, + "s.example.com|banner|/111/ks/categorytop/footer/300x250": 0.02, + "example6.com|banner|/111/cgs/ros/300x250": 0.01, + "example7.com|banner|/111/e/contents/footer_728x90": 0.06, + "example.com|banner|/111/k/categorytop/footer_right/300x250": 0.06, + "s.example.com|banner|/111/ks/top_320x50": 0.02, + "example5.com|banner|/111/t/map/middle_468x60": 0.04, + "example4.com|*|*": 0.07, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_6": 0.06, + "news.example.com|banner|/111/k/news/footer_right/300x250": 0.08, + "example4.com|banner|/111/kmag/1st_300x250": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_4": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_1": 0.07, + "example.com|banner|/111/k/btf/tv/footer_right_300x250": 0.01, + "s.example5.com|banner|/111/ts/list/300x250": 0.02, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_5": 0.06, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_10": 0.03, + "example.com|banner|/111/k/global_search/footer_right/300x250": 0.06, + "s.akiba-souken.com|banner|/111/as/1st_300x250": 0.03, + "bbs.example.com|banner|/111/k/itemview/bbs/160x600": 0.06, + "example.com|banner|/111/k/tv_728x90": 0.06, + "example3.com|*|*": 0.02, + "s.example.com|banner|/111/ks/ranking/middle_20/300x250": 0.05, + "s.example.com|banner|/111/ks/itemview/review/300x250_9": 0.03, + "example5.com|banner|/111/t/map/middle_left_300x250": 0.04, + "s.example.com|banner|/111/ks/itemview/review/300x250_12": 0.04, + "example.com|banner|/111/k/ranking/728x90": 0.06, + "example6.com|banner|/111/cg/ros/footer_right_300x250": 0.07, + "example6.com|banner|/111/cg/top_300x250": 0.09, + "example7.com|banner|/111/es/overlay/320x50": 0.07, + "s.example.com|banner|/111/ks/itemview/bbs/300x250": 0.08, + "example5.com|banner|/111/t/list/search_footer_right_300x250": 0.04, + "example.com|banner|/111/k/pricemenu/728x90": 0.06, + "s.example.com|banner|/111/ks/itemview/320x50_lazytest": 0.03, + "search.example.com|banner|/111/ks/itemlist/320x50": 0.03, + "s.example.com|banner|/111/ks/categorytop/middle/320x50": 0.06, + "example.com|*|*": 0.04, + "example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/footer_right/300x250": 0.04, + "s.example.com|banner|/111/ks/itemview/review/300x250_3": 0.05, + "example4.com|banner|/111/kmag/footer_left_300x250": 0.08, + "s.example.com|banner|/111/ks/itemview/bbs/300x250_2": 0.08, + "example.com|banner|/111/k/itemlist/728x90": 0.07, + "example.com|banner|/111/k/ranking/middle/left/300x250": 0.09, + "search.example.com|banner|/111/k/itemlist/160x600": 0.12, + "example2.com|banner|/111/kinarinopc/top_300x250": 0.01, + "s.example.com|banner|/111/ks/itemlist/footer/300x250": 0.02, + "example2.com|banner|/111/kinarino/login": 0.09, + "example5.com|banner|/111/t/special/4th_300x250": 0.02, + "s.example.com|banner|/111/ks/news/320x50": 0.05, + "s.example.com|banner|/111/ks/itemview/review/300x250_6": 0.03, + "example.com|banner|/111/k/ranking/footer_left/300x250": 0.03, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_2": 0.06, + "example4.com|banner|/111/kmag/3rd_300x250": 0.08, + "s.example.com|banner|/111/ks/itemview/300x250": 0.04, + "example.com|banner|/111/k/itemview/footer_left/300x250": 0.05, + "review.example.com|banner|/111/k/itemview/review/160x600": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_5": 0.06, + "example5.com|banner|/111/t/shop/shop_footer_left_300x250": 0.02, + "s.example5.com|banner|/111/ts/shop/middle/300x250": 0.04, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_6": 0.01, + "s.example.com|banner|/111/ks/ranking/middle_30/300x250": 0.03, + "example.com|banner|/111/k/top_2nd_300x250": 0.01, + "search.example.com|banner|/111/ks/itemlist/middle/320x50": 0.07, + "search.example.com|banner|/111/k/itemlist/footer_right/300x250": 0.06, + "s.example.com|banner|/111/ks/categorytop/320x50": 0.02, + "s.example.com|banner|/111/ks/pricemenu/320x50": 0.02, + "example.com|banner|/111/k/specsearch/footer/728x90": 0.01, + "example4.com|banner|/111/4ts/ros/video_320x180": 0.03, + "example5.com|banner|/111/t/matome/article/300x250": 0.04, + "s.example.com|banner|/111/ks/ranking/middle_10/300x250": 0.07, + "s.example.com|banner|/111/ks/ranking/320x50": 0.03, + "example.com|banner|/111/k/categorytop/300x250": 0.08, + "s.example.com|banner|/111/ks/itemview/h/320x50": 0.03, + "example2.com|banner|/111/kinarino/article": 0.13, + "anime.example7.com|banner|/111/ahs/overlay/320x50": 0.09, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_3": 0.06, + "example.com|banner|/111/k/pricemenu/footer_right/300x250": 0.06, + "s.example.com|banner|/111/ks/itemview/bbs/footer/300x250": 0.03, + "s.example.com|banner|/111/ks/global_search/300x250": 0.06, + "example2.com|*|*": 0.03, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_5": 0.08, + "review.example.com|*|*": 0.04, + "s.example.com|banner|/111/ks/itemview/review/footer/300x250": 0.03, + "example6.com|banner|/111/cg/ros/footer_left_300x250": 0.04, + "search.example.com|banner|/111/k/itemlist/footer_left/300x250": 0.04, + "example.com|banner|/111/k/itemview/h/160x600": 0.06, + "review.example.com|banner|/111/k/itemview/review/728x90": 0.05, + "s.example.com|banner|/111/ks/tv/overlay_320x50": 0.04, + "news.example.com|*|*": 0.07, + "example.com|banner|/111/k/ranking/middle/right/300x250": 0.03, + "s.example.com|banner|/111/ks/itemlist/300x250": 0.03, + "example6.com|*|*": 0.02, + "example4.com|banner|/111/ksmag/footer_300x250": 0.06, + "example7.com|banner|/111/e/overlay/728x90": 0.03, + "example4.com|banner|/111/kmag/2nd_300x250": 0.09, + "s.example.com|banner|/111/ks/itemview/review/300x250": 0.04, + "example.com|banner|/111/k/categorytop/728x90": 0.06, + "example5.com|banner|/111/t/shop/shop_footer_right_300x250": 0.02, + "bbs.example.com|banner|/111/k/itemview/footer_left/300x250": 0.04, + "*|*|*": 0.01, + "bbs.example.com|*|*": 0.02, + "s.example.com|banner|/111/ks/itemview/bbs_view/300x250_20": 0.06, + "example3.com|banner|/111/icotto_sp/article/footer_1st300x250": 0.04, + "s.example.com|banner|/111/ks/itemlist/320x50": 0.04, + "s.example.com|banner|/111/ks/itemview/footer/300x250": 0.03, + "s.example.com|banner|/111/ks/itemview/h/300x250": 0.03, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_3": 0.06, + "example3.com|banner|/111/icotto_pc/2nd_300x250": 0.08, + "example.com|banner|/111/k/pricemenu/300x250": 0.06, + "s.example.com|banner|/111/ks/tv/middle_300x250": 0.03, + "example7.com|banner|/111/es/contents/footer_buzz_300x250": 0.02, + "example.com|banner|/111/k/itemview/spec/160x600": 0.07, + "example5.com|banner|/111/t/map/middle_right_300x250": 0.04, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_left/300x250_2": 0.06, + "bbs.example.com|banner|/111/k/itemview/bbs/middle_right/300x250_4": 0.06, + "search.example.com|*|*": 0.02 + }, + "default": 0.01 + } + ], + "modelTimestamp": 1646254800, + "currency": "USD", + "skipRate": 0, + "floorsSchemaVersion": 2 } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json b/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json index 1f96d298b63..3c4dc6adb9a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/floors/provided-floors.json @@ -1,25 +1,25 @@ { - "data": { - "modelGroups": [ - { - "modelWeight": 100, - "currency": "NZD", - "schema": { - "fields": [ - "domain","mediaType", "size" - ] - }, - "values": { - "*|banner|*": 10, - "*|banner|320x250": 8, - "*|*|320x250": 6, - "www.example.com|*|320x250": 4, - "www.example.com|banner|320x250": 2, - "www.example.com|banner|*": 1, - "*|video|*": 800, - "*|video|640x480": 333 - } + "modelGroups": [ + { + "modelWeight": 100, + "currency": "NZD", + "schema": { + "fields": [ + "domain", + "mediaType", + "size" + ] + }, + "values": { + "*|banner|*": 10, + "*|banner|320x250": 8, + "*|*|320x250": 6, + "www.example.com|*|320x250": 4, + "www.example.com|banner|320x250": 2, + "www.example.com|banner|*": 1, + "*|video|*": 800, + "*|video|640x480": 333 } - ] - } + } + ] } From 344f16e66c84399d104e0e2a7f7f310b10a07973 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko Date: Tue, 26 Apr 2022 14:47:26 +0200 Subject: [PATCH 09/12] Tests: Fix unused param (#1840) --- .../org/prebid/server/functional/tests/AccountSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy index e679594ce07..925a2f78794 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AccountSpec.groovy @@ -200,7 +200,7 @@ class AccountSpec extends BaseSpec { and: "Default stored request with non-existing account" def ampStoredRequest = BidRequest.defaultStoredRequest.tap { site = Site.defaultSite - site.publisher.id = null + site.publisher.id = storedRequestAccount } and: "Save storedRequest into DB" From ac8b652ce5c752e40f43eb5dc1c5ef7c8172c872 Mon Sep 17 00:00:00 2001 From: Serhii Nahornyi Date: Wed, 27 Apr 2022 12:03:05 +0300 Subject: [PATCH 10/12] Prebid Server prepare release 1.87.1 --- extra/bundle/pom.xml | 6 +++--- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pom.xml | 4 ++-- extra/pom.xml | 4 ++-- pom.xml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index d15bc241e71..39699552821 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.1-SNAPSHOT + 1.87.1 ../../extra/pom.xml @@ -77,12 +77,12 @@ org.prebid prebid-server - 1.87.1-SNAPSHOT + 1.87.1 org.prebid.server.hooks.modules ortb2-blocking - 1.87.1-SNAPSHOT + 1.87.1 diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 2d550e0a6c0..309f12fff23 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 - 1.87.1-SNAPSHOT + 1.87.1 ortb2-blocking diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 464f8dd4e0f..9e43674b79e 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.1-SNAPSHOT + 1.87.1 ../../extra/pom.xml @@ -26,7 +26,7 @@ 11 11 - 1.87.1-SNAPSHOT + 1.87.1 1.18.22 diff --git a/extra/pom.xml b/extra/pom.xml index ee6d518b953..4b855a7e6d4 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 1.87.1-SNAPSHOT + 1.87.1 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 + 1.87.1 diff --git a/pom.xml b/pom.xml index fedb5571e77..3ea0ee55399 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.1-SNAPSHOT + 1.87.1 extra/pom.xml From 4ad67bf6dbb6aafa7a6cd74528d8e102c62a718d Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Wed, 27 Apr 2022 13:01:37 +0300 Subject: [PATCH 11/12] Prebid Server prepare for next development iteration (#1843) --- extra/bundle/pom.xml | 6 +++--- extra/modules/ortb2-blocking/pom.xml | 2 +- extra/modules/pom.xml | 4 ++-- extra/pom.xml | 4 ++-- pom.xml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 39699552821..7fcacb1cd54 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.1 + 1.88.0-SNAPSHOT ../../extra/pom.xml @@ -77,12 +77,12 @@ org.prebid prebid-server - 1.87.1 + 1.88.0-SNAPSHOT org.prebid.server.hooks.modules ortb2-blocking - 1.87.1 + 1.88.0-SNAPSHOT diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 309f12fff23..cfd74cc0189 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 - 1.87.1 + 1.88.0-SNAPSHOT ortb2-blocking diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 9e43674b79e..413bbe5e1ec 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.1 + 1.88.0-SNAPSHOT ../../extra/pom.xml @@ -26,7 +26,7 @@ 11 11 - 1.87.1 + 1.88.0-SNAPSHOT 1.18.22 diff --git a/extra/pom.xml b/extra/pom.xml index 4b855a7e6d4..4b9691068e5 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,14 +4,14 @@ org.prebid prebid-server-aggregator - 1.87.1 + 1.88.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 - 1.87.1 + HEAD diff --git a/pom.xml b/pom.xml index 3ea0ee55399..12162b85614 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 1.87.1 + 1.88.0-SNAPSHOT extra/pom.xml From fca2b1b6bc940379d547cb762fe2be1a5a68d001 Mon Sep 17 00:00:00 2001 From: Product AAX Date: Thu, 28 Apr 2022 14:32:11 +0530 Subject: [PATCH 12/12] Using GenericBidder instead of AaxBidder --- .../prebid/server/bidder/aax/AaxBidder.java | 90 ----------- .../config/bidder/AaxConfiguration.java | 4 +- .../server/bidder/aax/AaxBidderTest.java | 145 ------------------ 3 files changed, 2 insertions(+), 237 deletions(-) delete mode 100644 src/main/java/org/prebid/server/bidder/aax/AaxBidder.java delete mode 100644 src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java diff --git a/src/main/java/org/prebid/server/bidder/aax/AaxBidder.java b/src/main/java/org/prebid/server/bidder/aax/AaxBidder.java deleted file mode 100644 index 3fd232428b2..00000000000 --- a/src/main/java/org/prebid/server/bidder/aax/AaxBidder.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.prebid.server.bidder.aax; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.BidResponse; -import com.iab.openrtb.response.SeatBid; -import io.vertx.core.http.HttpMethod; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpCall; -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.HttpUtil; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -public class AaxBidder implements Bidder { - - private final String endpointUrl; - private final JacksonMapper mapper; - - public AaxBidder(String endpointUrl, JacksonMapper mapper) { - this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest bidRequest) { - return Result.withValue(HttpRequest.builder() - .method(HttpMethod.POST) - .headers(HttpUtil.headers()) - .uri(endpointUrl) - .body(mapper.encodeToBytes(bidRequest)) - .payload(bidRequest) - .build()); - } - - @Override - public final Result> makeBids(HttpCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); - } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); - } - } - - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse) { - if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { - return Collections.emptyList(); - } - - final String currency = bidResponse.getCur(); - return bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .filter(Objects::nonNull) - .map(bid -> BidderBid.of(bid, resolveBidType(bid.getImpid(), bidRequest.getImp()), currency)) - .collect(Collectors.toList()); - } - - private static BidType resolveBidType(String impId, List imps) { - for (Imp imp : imps) { - if (Objects.equals(impId, imp.getId())) { - if (imp.getBanner() != null) { - return BidType.banner; - } else if (imp.getVideo() != null) { - return BidType.video; - } else if (imp.getXNative() != null) { - return BidType.xNative; - } else if (imp.getAudio() != null) { - return BidType.audio; - } - } - } - - return BidType.banner; - } -} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java index 615c581adcc..4a232c39bec 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AaxConfiguration.java @@ -1,7 +1,7 @@ package org.prebid.server.spring.config.bidder; import org.prebid.server.bidder.BidderDeps; -import org.prebid.server.bidder.aax.AaxBidder; +import org.prebid.server.bidder.GenericBidder; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -37,7 +37,7 @@ BidderDeps aaxBidderDeps(BidderConfigurationProperties aaxConfigurationPropertie return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(aaxConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new AaxBidder(resolveEndpoint(config.getEndpoint(), externalUrl), mapper)) + .bidderCreator(config -> new GenericBidder(resolveEndpoint(config.getEndpoint(), externalUrl), mapper)) .assemble(); } diff --git a/src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java b/src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java deleted file mode 100644 index 2ec81cc06c9..00000000000 --- a/src/test/java/org/prebid/server/bidder/aax/AaxBidderTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.prebid.server.bidder.aax; - -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.Before; -import org.junit.Test; -import org.prebid.server.VertxTest; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.bidder.model.HttpCall; -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 java.util.List; -import java.util.function.Function; - -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.proto.openrtb.ext.response.BidType.banner; - -public class AaxBidderTest extends VertxTest { - - private static final String ENDPOINT_URL = "https://test.aax.net?src=external.prebidserver.com"; - - private AaxBidder aaxBidder; - - @Before - public void setup() { - aaxBidder = new AaxBidder(ENDPOINT_URL, jacksonMapper); - } - - @Test - public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new AaxBidder("invalid_url", jacksonMapper)); - } - - @Test - public void makeHttpRequestsShouldNotModifyIncomingRequest() { - // given - final BidRequest bidRequest = givenBidRequest(); - - // when - final Result>> result; - result = aaxBidder.makeHttpRequests(bidRequest); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1) - .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class)) - .containsExactly(bidRequest); - } - - @Test - public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { - // given - final HttpCall httpCall = sampleHttpCall(givenBidRequest(), "invalid response"); - - // when - final Result> result = aaxBidder.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).hasSize(1) - .allMatch(error -> error.getType() == BidderError.Type.bad_server_response - && error.getMessage().startsWith("Failed to decode: Unrecognized token")); - } - - @Test - public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { - // given - final HttpCall httpCall; - httpCall = sampleHttpCall(givenBidRequest(), mapper.writeValueAsString(null)); - - // when - final Result> result = aaxBidder.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws JsonProcessingException { - // given - final HttpCall httpCall; - httpCall = sampleHttpCall(null, mapper.writeValueAsString(BidResponse.builder().build())); - - // when - final Result> result = aaxBidder.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); - } - - @Test - public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessingException { - // given - final HttpCall httpCall = sampleHttpCall( - givenBidRequest(), - mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("123")))); - - // when - final Result> result = aaxBidder.makeBids(httpCall, null); - - // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); - } - - private static BidResponse sampleBidResponse(Function bidCustomizer) { - return BidResponse.builder() - .cur("USD") - .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) - .build())) - .build(); - } - - private static HttpCall sampleHttpCall(BidRequest bidRequest, String body) { - return HttpCall.success( - HttpRequest.builder().payload(bidRequest).build(), - HttpResponse.of(200, null, body), - null); - } - - private static BidRequest givenBidRequest() { - return BidRequest.builder() - .id("request_id") - .imp(singletonList(Imp.builder() - .id("imp_id") - .ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createObjectNode()))) - .build())) - .build(); - } -}